mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 19:49:16 +00:00
Huge refactor: rename photo to media
To prepare for video support Migrate database rename tables and columns: - photo to media - photo_url to media_url - photo_exif to media_exif - Update api accordingly
This commit is contained in:
9
api/database/migrations/008_video_support.down.sql
Normal file
9
api/database/migrations/008_video_support.down.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
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;
|
||||
9
api/database/migrations/008_video_support.up.sql
Normal file
9
api/database/migrations/008_video_support.up.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
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 CHANGE COLUMN photo_id media_id int NOT NULL AUTO_INCREMENT;
|
||||
ALTER TABLE media_url CHANGE COLUMN photo_id media_id int NOT NULL;
|
||||
ALTER TABLE media_url CHANGE COLUMN photo_name media_name varchar(512) NOT NULL;
|
||||
ALTER TABLE share_token CHANGE COLUMN photo_id media_id int;
|
||||
@@ -24,12 +24,12 @@ models:
|
||||
model: github.com/99designs/gqlgen/graphql.IntID
|
||||
User:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.User
|
||||
Photo:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.Photo
|
||||
PhotoURL:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.PhotoURL
|
||||
PhotoEXIF:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.PhotoEXIF
|
||||
Media:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.Media
|
||||
MediaURL:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.MediaURL
|
||||
MediaEXIF:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.MediaEXIF
|
||||
Album:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.Album
|
||||
ShareToken:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,13 @@ type Filter struct {
|
||||
Offset *int `json:"offset"`
|
||||
}
|
||||
|
||||
type MediaDownload struct {
|
||||
Title string `json:"title"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Key string `json:"key"`
|
||||
Type NotificationType `json:"type"`
|
||||
@@ -33,13 +40,6 @@ type Notification struct {
|
||||
Timeout *int `json:"timeout"`
|
||||
}
|
||||
|
||||
type PhotoDownload struct {
|
||||
Title string `json:"title"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type ScannerResult struct {
|
||||
Finished bool `json:"finished"`
|
||||
Success bool `json:"success"`
|
||||
@@ -50,7 +50,7 @@ type ScannerResult struct {
|
||||
type SearchResult struct {
|
||||
Query string `json:"query"`
|
||||
Albums []*Album `json:"albums"`
|
||||
Photos []*Photo `json:"photos"`
|
||||
Media []*Media `json:"media"`
|
||||
}
|
||||
|
||||
// General public information about the site
|
||||
|
||||
101
api/graphql/models/media.go
Normal file
101
api/graphql/models/media.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path"
|
||||
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
MediaID int
|
||||
Title string
|
||||
Path string
|
||||
PathHash string
|
||||
AlbumId int
|
||||
ExifId *int
|
||||
Favorite bool
|
||||
}
|
||||
|
||||
func (p *Media) ID() int {
|
||||
return p.MediaID
|
||||
}
|
||||
|
||||
type MediaPurpose string
|
||||
|
||||
const (
|
||||
PhotoThumbnail MediaPurpose = "thumbnail"
|
||||
PhotoHighRes MediaPurpose = "high-res"
|
||||
MediaOriginal MediaPurpose = "original"
|
||||
VideoWeb MediaPurpose = "video-web"
|
||||
)
|
||||
|
||||
type MediaURL struct {
|
||||
UrlID int
|
||||
MediaId int
|
||||
MediaName string
|
||||
Width int
|
||||
Height int
|
||||
Purpose MediaPurpose
|
||||
ContentType string
|
||||
}
|
||||
|
||||
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); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &media, nil
|
||||
}
|
||||
|
||||
func NewMediaFromRows(rows *sql.Rows) ([]*Media, error) {
|
||||
medias := make([]*Media, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var media Media
|
||||
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
medias = append(medias, &media)
|
||||
}
|
||||
|
||||
rows.Close()
|
||||
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func (p *MediaURL) URL() string {
|
||||
|
||||
imageUrl := utils.ApiEndpointUrl()
|
||||
imageUrl.Path = path.Join(imageUrl.Path, "photo", p.MediaName)
|
||||
|
||||
return imageUrl.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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &url, nil
|
||||
}
|
||||
|
||||
func NewMediaURLFromRows(rows *sql.Rows) ([]*MediaURL, error) {
|
||||
urls := make([]*MediaURL, 0)
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
urls = append(urls, &url)
|
||||
}
|
||||
|
||||
rows.Close()
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type PhotoEXIF struct {
|
||||
type MediaEXIF struct {
|
||||
ExifID int
|
||||
Camera *string
|
||||
Maker *string
|
||||
@@ -20,16 +20,16 @@ type PhotoEXIF struct {
|
||||
ExposureProgram *int
|
||||
}
|
||||
|
||||
func (exif *PhotoEXIF) Photo() *Photo {
|
||||
func (exif *MediaEXIF) Media() *Media {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (exif *PhotoEXIF) ID() int {
|
||||
func (exif *MediaEXIF) ID() int {
|
||||
return exif.ExifID
|
||||
}
|
||||
|
||||
func NewPhotoExifFromRow(row *sql.Row) (*PhotoEXIF, error) {
|
||||
exif := PhotoEXIF{}
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -1,101 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path"
|
||||
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
)
|
||||
|
||||
type Photo struct {
|
||||
PhotoID int
|
||||
Title string
|
||||
Path string
|
||||
PathHash string
|
||||
AlbumId int
|
||||
ExifId *int
|
||||
Favorite bool
|
||||
}
|
||||
|
||||
func (p *Photo) ID() int {
|
||||
return p.PhotoID
|
||||
}
|
||||
|
||||
type MediaPurpose string
|
||||
|
||||
const (
|
||||
PhotoThumbnail MediaPurpose = "thumbnail"
|
||||
PhotoHighRes MediaPurpose = "high-res"
|
||||
MediaOriginal MediaPurpose = "original"
|
||||
VideoWeb MediaPurpose = "video-web"
|
||||
)
|
||||
|
||||
type PhotoURL struct {
|
||||
UrlID int
|
||||
PhotoId int
|
||||
PhotoName string
|
||||
Width int
|
||||
Height int
|
||||
Purpose MediaPurpose
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func NewPhotoFromRow(row *sql.Row) (*Photo, error) {
|
||||
photo := Photo{}
|
||||
|
||||
if err := row.Scan(&photo.PhotoID, &photo.Title, &photo.Path, &photo.PathHash, &photo.AlbumId, &photo.ExifId, &photo.Favorite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &photo, nil
|
||||
}
|
||||
|
||||
func NewPhotosFromRows(rows *sql.Rows) ([]*Photo, error) {
|
||||
photos := make([]*Photo, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var photo Photo
|
||||
if err := rows.Scan(&photo.PhotoID, &photo.Title, &photo.Path, &photo.PathHash, &photo.AlbumId, &photo.ExifId, &photo.Favorite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
photos = append(photos, &photo)
|
||||
}
|
||||
|
||||
rows.Close()
|
||||
|
||||
return photos, nil
|
||||
}
|
||||
|
||||
func (p *PhotoURL) URL() string {
|
||||
|
||||
imageUrl := utils.ApiEndpointUrl()
|
||||
imageUrl.Path = path.Join(imageUrl.Path, "photo", p.PhotoName)
|
||||
|
||||
return imageUrl.String()
|
||||
}
|
||||
|
||||
func NewPhotoURLFromRow(row *sql.Row) (*PhotoURL, error) {
|
||||
url := PhotoURL{}
|
||||
|
||||
if err := row.Scan(&url.UrlID, &url.PhotoId, &url.PhotoName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &url, nil
|
||||
}
|
||||
|
||||
func NewPhotoURLFromRows(rows *sql.Rows) ([]*PhotoURL, error) {
|
||||
urls := make([]*PhotoURL, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var url PhotoURL
|
||||
if err := rows.Scan(&url.UrlID, &url.PhotoId, &url.PhotoName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls = append(urls, &url)
|
||||
}
|
||||
|
||||
rows.Close()
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type ShareToken struct {
|
||||
Expire *time.Time
|
||||
Password *string
|
||||
AlbumID *int
|
||||
PhotoID *int
|
||||
MediaID *int
|
||||
}
|
||||
|
||||
func (share *ShareToken) Token() string {
|
||||
@@ -26,7 +26,7 @@ func (share *ShareToken) ID() int {
|
||||
func NewShareTokenFromRow(row *sql.Row) (*ShareToken, error) {
|
||||
token := ShareToken{}
|
||||
|
||||
if err := row.Scan(&token.TokenID, &token.Value, &token.OwnerID, &token.Expire, &token.Password, &token.AlbumID, &token.PhotoID); err != nil {
|
||||
if err := row.Scan(&token.TokenID, &token.Value, &token.OwnerID, &token.Expire, &token.Password, &token.AlbumID, &token.MediaID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func NewShareTokensFromRows(rows *sql.Rows) ([]*ShareToken, error) {
|
||||
|
||||
for rows.Next() {
|
||||
var token ShareToken
|
||||
if err := rows.Scan(&token.TokenID, &token.Value, &token.OwnerID, &token.Expire, &token.Password, &token.AlbumID, &token.PhotoID); err != nil {
|
||||
if err := rows.Scan(&token.TokenID, &token.Value, &token.OwnerID, &token.Expire, &token.Password, &token.AlbumID, &token.MediaID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, &token)
|
||||
|
||||
@@ -22,7 +22,7 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onl
|
||||
|
||||
var rows *sql.Rows
|
||||
|
||||
filterEmpty := " AND EXISTS (SELECT * FROM photo WHERE album_id = album.album_id) "
|
||||
filterEmpty := " AND EXISTS (SELECT * FROM media WHERE album_id = album.album_id) "
|
||||
if showEmpty != nil && *showEmpty == true {
|
||||
filterEmpty = ""
|
||||
}
|
||||
@@ -72,34 +72,34 @@ func (r *Resolver) Album() api.AlbumResolver {
|
||||
|
||||
type albumResolver struct{ *Resolver }
|
||||
|
||||
func (r *albumResolver) Photos(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Photo, error) {
|
||||
func (r *albumResolver) Media(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Media, error) {
|
||||
|
||||
filterSQL, err := filter.FormatSQL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
photoRows, err := r.Database.Query(`
|
||||
SELECT photo.* FROM album, photo
|
||||
WHERE album.album_id = ? AND photo.album_id = album.album_id
|
||||
AND photo.photo_id IN (
|
||||
SELECT photo_id FROM photo_url WHERE photo_url.photo_id = photo.photo_id
|
||||
mediaRows, err := r.Database.Query(`
|
||||
SELECT media.* FROM album, media
|
||||
WHERE album.album_id = ? AND media.album_id = album.album_id
|
||||
AND media.media_id IN (
|
||||
SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id
|
||||
)
|
||||
`+filterSQL, obj.AlbumID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer photoRows.Close()
|
||||
defer mediaRows.Close()
|
||||
|
||||
photos, err := models.NewPhotosFromRows(photoRows)
|
||||
media, err := models.NewMediaFromRows(mediaRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return photos, nil
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Photo, error) {
|
||||
func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) {
|
||||
|
||||
row := r.Database.QueryRow(`
|
||||
WITH recursive sub_albums AS (
|
||||
@@ -108,14 +108,14 @@ func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*mode
|
||||
SELECT child.* FROM album AS child JOIN sub_albums ON child.parent_album = sub_albums.album_id
|
||||
)
|
||||
|
||||
SELECT * FROM photo WHERE photo.album_id IN (
|
||||
SELECT * FROM media WHERE media.album_id IN (
|
||||
SELECT album_id FROM sub_albums
|
||||
) AND photo.photo_id IN (
|
||||
SELECT photo_id FROM photo_url WHERE photo_url.photo_id = photo.photo_id
|
||||
) AND media.media_id IN (
|
||||
SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id
|
||||
) LIMIT 1
|
||||
`, obj.AlbumID)
|
||||
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
media, err := models.NewMediaFromRow(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -124,7 +124,7 @@ func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*mode
|
||||
}
|
||||
}
|
||||
|
||||
return photo, nil
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (r *albumResolver) SubAlbums(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Album, error) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/viktorstrate/photoview/api/scanner"
|
||||
)
|
||||
|
||||
func (r *queryResolver) MyPhotos(ctx context.Context, filter *models.Filter) ([]*models.Photo, error) {
|
||||
func (r *queryResolver) MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
@@ -25,52 +25,52 @@ func (r *queryResolver) MyPhotos(ctx context.Context, filter *models.Filter) ([]
|
||||
}
|
||||
|
||||
rows, err := r.Database.Query(`
|
||||
SELECT photo.* FROM photo, album
|
||||
WHERE photo.album_id = album.album_id AND album.owner_id = ?
|
||||
AND photo.photo_id IN (
|
||||
SELECT photo_id FROM photo_url WHERE photo_url.photo_id = photo.photo_id
|
||||
SELECT media.* FROM media, album
|
||||
WHERE media.album_id = album.album_id AND album.owner_id = ?
|
||||
AND media.media_id IN (
|
||||
SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id
|
||||
)
|
||||
`+filterSQL, user.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewPhotosFromRows(rows)
|
||||
return models.NewMediaFromRows(rows)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Photo(ctx context.Context, id int) (*models.Photo, error) {
|
||||
func (r *queryResolver) Media(ctx context.Context, id int) (*models.Media, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
row := r.Database.QueryRow(`
|
||||
SELECT photo.* FROM photo
|
||||
JOIN album ON photo.album_id = album.album_id
|
||||
WHERE photo.photo_id = ? AND album.owner_id = ?
|
||||
AND photo.photo_id IN (
|
||||
SELECT photo_id FROM photo_url WHERE photo_url.photo_id = photo.photo_id
|
||||
SELECT media.* FROM media
|
||||
JOIN album ON media.album_id = album.album_id
|
||||
WHERE media.media_id = ? AND album.owner_id = ?
|
||||
AND media.media_id IN (
|
||||
SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id
|
||||
)
|
||||
`, id, user.UserID)
|
||||
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
media, err := models.NewMediaFromRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return photo, nil
|
||||
return media, nil
|
||||
}
|
||||
|
||||
type photoResolver struct {
|
||||
type mediaResolver struct {
|
||||
*Resolver
|
||||
}
|
||||
|
||||
func (r *Resolver) Photo() api.PhotoResolver {
|
||||
return &photoResolver{r}
|
||||
func (r *Resolver) Media() api.MediaResolver {
|
||||
return &mediaResolver{r}
|
||||
}
|
||||
|
||||
func (r *photoResolver) Shares(ctx context.Context, obj *models.Photo) ([]*models.ShareToken, error) {
|
||||
rows, err := r.Database.Query("SELECT * FROM share_token WHERE photo_id = ?", obj.PhotoID)
|
||||
func (r *mediaResolver) Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error) {
|
||||
rows, err := r.Database.Query("SELECT * FROM share_token WHERE media_id = ?", obj.MediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -78,21 +78,21 @@ func (r *photoResolver) Shares(ctx context.Context, obj *models.Photo) ([]*model
|
||||
return models.NewShareTokensFromRows(rows)
|
||||
}
|
||||
|
||||
func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error) {
|
||||
func (r *mediaResolver) Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error) {
|
||||
|
||||
rows, err := r.Database.Query("SELECT * FROM photo_url WHERE photo_id = ?", obj.PhotoID)
|
||||
rows, err := r.Database.Query("SELECT * FROM media_url WHERE media_id = ?", obj.MediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
photoUrls, err := models.NewPhotoURLFromRows(rows)
|
||||
mediaUrls, err := models.NewMediaURLFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloads := make([]*models.PhotoDownload, 0)
|
||||
downloads := make([]*models.MediaDownload, 0)
|
||||
|
||||
for _, url := range photoUrls {
|
||||
for _, url := range mediaUrls {
|
||||
|
||||
var title string
|
||||
switch {
|
||||
@@ -104,7 +104,7 @@ func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*mo
|
||||
title = "Large"
|
||||
}
|
||||
|
||||
downloads = append(downloads, &models.PhotoDownload{
|
||||
downloads = append(downloads, &models.MediaDownload{
|
||||
Title: title,
|
||||
Width: url.Width,
|
||||
Height: url.Height,
|
||||
@@ -115,23 +115,23 @@ func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*mo
|
||||
return downloads, nil
|
||||
}
|
||||
|
||||
func (r *photoResolver) HighRes(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
|
||||
func (r *mediaResolver) HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
|
||||
// Try high res first, then
|
||||
web_types_questions := strings.Repeat("?,", len(scanner.WebMimetypes))[:len(scanner.WebMimetypes)*2-1]
|
||||
args := make([]interface{}, 0)
|
||||
args = append(args, obj.PhotoID, models.PhotoHighRes, models.MediaOriginal)
|
||||
args = append(args, obj.MediaID, models.PhotoHighRes, models.MediaOriginal)
|
||||
for _, webtype := range scanner.WebMimetypes {
|
||||
args = append(args, webtype)
|
||||
}
|
||||
|
||||
row := r.Database.QueryRow(`
|
||||
SELECT * FROM photo_url WHERE photo_id = ? AND
|
||||
SELECT * FROM media_url WHERE media_id = ? AND
|
||||
(
|
||||
purpose = ? OR (purpose = ? AND content_type IN (`+web_types_questions+`))
|
||||
) LIMIT 1
|
||||
`, args...)
|
||||
|
||||
url, err := models.NewPhotoURLFromRow(row)
|
||||
url, err := models.NewMediaURLFromRow(row)
|
||||
if err != nil {
|
||||
log.Printf("Error: Could not query highres: %s\n", err)
|
||||
return nil, err
|
||||
@@ -140,10 +140,10 @@ func (r *photoResolver) HighRes(ctx context.Context, obj *models.Photo) (*models
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (r *photoResolver) Thumbnail(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?", obj.PhotoID, models.PhotoThumbnail)
|
||||
func (r *mediaResolver) Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM media_url WHERE media_id = ? AND purpose = ?", obj.MediaID, models.PhotoThumbnail)
|
||||
|
||||
url, err := models.NewPhotoURLFromRow(row)
|
||||
url, err := models.NewMediaURLFromRow(row)
|
||||
if err != nil {
|
||||
log.Printf("Error: Could not query thumbnail: %s\n", err)
|
||||
return nil, err
|
||||
@@ -152,15 +152,15 @@ func (r *photoResolver) Thumbnail(ctx context.Context, obj *models.Photo) (*mode
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.Album, error) {
|
||||
row := r.Database.QueryRow("SELECT album.* from photo JOIN album ON photo.album_id = album.album_id WHERE photo_id = ?", obj.PhotoID)
|
||||
func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) {
|
||||
row := r.Database.QueryRow("SELECT album.* from media JOIN album ON media.album_id = album.album_id WHERE media_id = ?", obj.MediaID)
|
||||
return models.NewAlbumFromRow(row)
|
||||
}
|
||||
|
||||
func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.PhotoEXIF, error) {
|
||||
row := r.Database.QueryRow("SELECT photo_exif.* FROM photo NATURAL JOIN photo_exif WHERE photo.photo_id = ?", obj.PhotoID)
|
||||
func (r *mediaResolver) Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error) {
|
||||
row := r.Database.QueryRow("SELECT media_exif.* FROM media NATURAL JOIN media_exif WHERE media.media_id = ?", obj.MediaID)
|
||||
|
||||
exif, err := models.NewPhotoExifFromRow(row)
|
||||
exif, err := models.NewMediaExifFromRow(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -172,23 +172,23 @@ func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.Ph
|
||||
return exif, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) FavoritePhoto(ctx context.Context, photoID int, favorite bool) (*models.Photo, error) {
|
||||
func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) {
|
||||
|
||||
user := auth.UserFromContext(ctx)
|
||||
|
||||
row := r.Database.QueryRow("SELECT photo.* FROM photo JOIN album ON photo.album_id = album.album_id WHERE photo.photo_id = ? AND album.owner_id = ?", photoID, user.UserID)
|
||||
row := r.Database.QueryRow("SELECT media.* FROM media JOIN album ON media.album_id = album.album_id WHERE media.media_id = ? AND album.owner_id = ?", mediaID, user.UserID)
|
||||
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
media, err := models.NewMediaFromRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = r.Database.Exec("UPDATE photo SET favorite = ? WHERE photo_id = ?", favorite, photo.PhotoID)
|
||||
_, err = r.Database.Exec("UPDATE media SET favorite = ? WHERE media_id = ?", favorite, media.MediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
photo.Favorite = favorite
|
||||
media.Favorite = favorite
|
||||
|
||||
return photo, nil
|
||||
return media, nil
|
||||
}
|
||||
|
||||
@@ -2,23 +2,23 @@ package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/viktorstrate/photoview/api/graphql/auth"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
)
|
||||
|
||||
func (r *Resolver) Search(ctx context.Context, query string, _limitPhotos *int, _limitAlbums *int) (*models.SearchResult, error) {
|
||||
func (r *Resolver) Search(ctx context.Context, query string, _limitMedia *int, _limitAlbums *int) (*models.SearchResult, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
limitPhotos := 10
|
||||
limitMedia := 10
|
||||
limitAlbums := 10
|
||||
|
||||
if _limitPhotos != nil {
|
||||
limitPhotos = *_limitPhotos
|
||||
if _limitMedia != nil {
|
||||
limitMedia = *_limitMedia
|
||||
}
|
||||
|
||||
if _limitAlbums != nil {
|
||||
@@ -28,20 +28,19 @@ func (r *Resolver) Search(ctx context.Context, query string, _limitPhotos *int,
|
||||
wildQuery := "%" + query + "%"
|
||||
|
||||
photoRows, err := r.Database.Query(`
|
||||
SELECT photo.* FROM photo JOIN album ON photo.album_id = album.album_id
|
||||
WHERE album.owner_id = ? AND ( photo.title LIKE ? OR photo.path LIKE ? )
|
||||
SELECT media.* FROM media JOIN album ON media.album_id = album.album_id
|
||||
WHERE album.owner_id = ? AND ( media.title LIKE ? OR media.path LIKE ? )
|
||||
ORDER BY (
|
||||
case when photo.title LIKE ? then 2
|
||||
when photo.path LIKE ? then 1
|
||||
case when media.title LIKE ? then 2
|
||||
when media.path LIKE ? then 1
|
||||
end ) DESC
|
||||
LIMIT ?
|
||||
`, user.UserID, wildQuery, wildQuery, wildQuery, wildQuery, limitPhotos)
|
||||
`, user.UserID, wildQuery, wildQuery, wildQuery, wildQuery, limitMedia)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: searching photos %s", err)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "searching media")
|
||||
}
|
||||
|
||||
photos, err := models.NewPhotosFromRows(photoRows)
|
||||
photos, err := models.NewMediaFromRows(photoRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -56,8 +55,7 @@ func (r *Resolver) Search(ctx context.Context, query string, _limitPhotos *int,
|
||||
LIMIT ?
|
||||
`, user.UserID, wildQuery, wildQuery, wildQuery, wildQuery, limitAlbums)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: searching albums %s", err)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "searching albums")
|
||||
}
|
||||
|
||||
albums, err := models.NewAlbumsFromRows(albumRows)
|
||||
@@ -67,7 +65,7 @@ func (r *Resolver) Search(ctx context.Context, query string, _limitPhotos *int,
|
||||
|
||||
result := models.SearchResult{
|
||||
Query: query,
|
||||
Photos: photos,
|
||||
Media: photos,
|
||||
Albums: albums,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ func (r *shareTokenResolver) Album(ctx context.Context, obj *models.ShareToken)
|
||||
return album, nil
|
||||
}
|
||||
|
||||
func (r *shareTokenResolver) Photo(ctx context.Context, obj *models.ShareToken) (*models.Photo, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM photo WHERE photo.photo_id = ?", obj.PhotoID)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
func (r *shareTokenResolver) Media(ctx context.Context, obj *models.ShareToken) (*models.Media, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM media WHERE media.media_id = ?", obj.MediaID)
|
||||
media, err := models.NewMediaFromRow(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -52,7 +52,7 @@ func (r *shareTokenResolver) Photo(ctx context.Context, obj *models.ShareToken)
|
||||
}
|
||||
}
|
||||
|
||||
return photo, nil
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (r *shareTokenResolver) HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error) {
|
||||
@@ -158,17 +158,17 @@ func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *
|
||||
Expire: expire,
|
||||
Password: password,
|
||||
AlbumID: &albumID,
|
||||
PhotoID: nil,
|
||||
MediaID: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SharePhoto(ctx context.Context, photoID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||
func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
rows, err := r.Database.Query("SELECT owner_id FROM album, photo WHERE photo.photo_id = ? AND photo.album_id = album.album_id AND album.owner_id = ?", photoID, user.UserID)
|
||||
rows, err := r.Database.Query("SELECT owner_id FROM album, media WHERE media.media_id = ? AND media.album_id = album.album_id AND album.owner_id = ?", mediaID, user.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func (r *mutationResolver) SharePhoto(ctx context.Context, photoID int, expire *
|
||||
}
|
||||
|
||||
token := utils.GenerateToken()
|
||||
res, err := r.Database.Exec("INSERT INTO share_token (value, owner_id, expire, password, photo_id) VALUES (?, ?, ?, ?, ?)", token, user.UserID, expire, hashed_password, photoID)
|
||||
res, err := r.Database.Exec("INSERT INTO share_token (value, owner_id, expire, password, media_id) VALUES (?, ?, ?, ?, ?)", token, user.UserID, expire, hashed_password, mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (r *mutationResolver) SharePhoto(ctx context.Context, photoID int, expire *
|
||||
Expire: expire,
|
||||
Password: password,
|
||||
AlbumID: nil,
|
||||
PhotoID: &photoID,
|
||||
MediaID: &mediaID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ func (r *mutationResolver) DeleteShareToken(ctx context.Context, tokenValue stri
|
||||
}
|
||||
|
||||
if _, err := r.Database.Exec("DELETE FROM share_token WHERE token_id = ?", token.TokenID); err != nil {
|
||||
return nil, errors.Wrapf(err, "Error occurred when trying to delete share token (%s) from database", tokenValue)
|
||||
return nil, errors.Wrapf(err, "failed to delete share token (%s) from database", tokenValue)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
@@ -240,7 +240,7 @@ func (r *mutationResolver) ProtectShareToken(ctx context.Context, tokenValue str
|
||||
|
||||
_, err = r.Database.Exec("UPDATE share_token SET password = ? WHERE token_id = ?", hashed_password, token.TokenID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to update password for share token")
|
||||
return nil, errors.Wrap(err, "failed to update password for share token")
|
||||
}
|
||||
|
||||
updatedToken := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue)
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
scalar Time
|
||||
|
||||
enum Role {
|
||||
admin
|
||||
user
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
username: String!
|
||||
albums: [Album]
|
||||
# Local filepath for the user's photos
|
||||
rootPath: String!
|
||||
admin: Boolean
|
||||
shareTokens: [ShareToken]
|
||||
}
|
||||
|
||||
type Album {
|
||||
id: ID!
|
||||
title: String
|
||||
photos: [Photo]
|
||||
subAlbums: [Album]
|
||||
parentAlbum: Album
|
||||
owner: User!
|
||||
path: String
|
||||
|
||||
shares: [ShareToken]
|
||||
}
|
||||
|
||||
type PhotoURL {
|
||||
# URL for previewing the image
|
||||
url: String
|
||||
# Width of the image in pixels
|
||||
width: Int
|
||||
# Height of the image in pixels
|
||||
height: Int
|
||||
}
|
||||
|
||||
type PhotoDownload {
|
||||
title: String
|
||||
url: String
|
||||
}
|
||||
|
||||
type PhotoEXIF {
|
||||
photo: Photo
|
||||
camera: String
|
||||
maker: String
|
||||
lens: String
|
||||
dateShot: Time
|
||||
fileSize: String
|
||||
exposure: String
|
||||
aperture: Float
|
||||
iso: Int
|
||||
focalLength: String
|
||||
flash: String
|
||||
}
|
||||
|
||||
type Photo {
|
||||
id: ID!
|
||||
title: String
|
||||
# Local filepath for the photo
|
||||
path: String
|
||||
# URL to display the photo in full resolution
|
||||
original: PhotoURL
|
||||
# URL to display the photo in a smaller resolution
|
||||
thumbnail: PhotoURL
|
||||
# The album that holds the photo
|
||||
album: Album!
|
||||
exif: PhotoEXIF
|
||||
|
||||
shares: [ShareToken]
|
||||
downloads: [PhotoDownload]
|
||||
}
|
||||
|
||||
type ShareToken {
|
||||
token: ID!
|
||||
owner: User!
|
||||
# Optional expire date
|
||||
expire: Time!
|
||||
# Optional password
|
||||
# password: String
|
||||
|
||||
album: Album
|
||||
photo: Photo
|
||||
}
|
||||
|
||||
type SiteInfo {
|
||||
initialSetup: Boolean!
|
||||
}
|
||||
|
||||
type AuthorizeResult {
|
||||
success: Boolean!
|
||||
status: String
|
||||
token: String
|
||||
}
|
||||
|
||||
type ScannerResult {
|
||||
finished: Boolean!
|
||||
success: Boolean!
|
||||
progress: Float
|
||||
message: String
|
||||
}
|
||||
|
||||
type Result {
|
||||
success: Boolean!
|
||||
errorMessage: String
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
scannerStatusUpdate: ScannerResult
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
authorizeUser(username: String!, password: String!): AuthorizeResult!
|
||||
|
||||
registerUser(
|
||||
username: String!
|
||||
password: String!
|
||||
rootPath: String!
|
||||
): AuthorizeResult!
|
||||
|
||||
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken
|
||||
sharePhoto(photoId: ID!, expire: Time, password: String): ShareToken
|
||||
|
||||
deleteShareToken(token: ID!): ShareToken
|
||||
|
||||
setAdmin(userId: ID!, admin: Boolean!): Result!
|
||||
|
||||
scanAll: ScannerResult!
|
||||
scanUser(userId: ID!): ScannerResult!
|
||||
|
||||
initialSetupWizard(
|
||||
username: String!
|
||||
password: String!
|
||||
rootPath: String!
|
||||
): AuthorizeResult
|
||||
|
||||
updateUser(id: ID!, username: String, rootPath: String, admin: Boolean): User
|
||||
createUser(id: ID, username: String, rootPath: String, admin: Boolean): User
|
||||
deleteUser(id: ID!): User
|
||||
|
||||
changeUserPassword(id: ID!, newPassword: String!): Result
|
||||
}
|
||||
|
||||
type Query {
|
||||
siteInfo: SiteInfo
|
||||
|
||||
myUser: User
|
||||
user: [User]
|
||||
|
||||
myAlbums: [Album]
|
||||
album(id: ID): Album
|
||||
|
||||
myPhotos: [Photo]
|
||||
photo(id: ID!): Photo
|
||||
|
||||
shareToken(token: ID!): ShareToken
|
||||
|
||||
albumShares(id: ID!, password: String): [ShareToken]
|
||||
photoShares(id: ID!, password: String): [ShareToken]
|
||||
}
|
||||
@@ -27,21 +27,21 @@ type Query {
|
||||
filter: Filter
|
||||
"Return only albums from the root directory of the user"
|
||||
onlyRoot: Boolean
|
||||
"Return also albums with no photos directly in them"
|
||||
"Return also albums with no media directly in them"
|
||||
showEmpty: Boolean
|
||||
): [Album!]!
|
||||
"Get album by id, user must own the album or be admin"
|
||||
album(id: Int!): Album!
|
||||
|
||||
"List of photos owned by the logged in user"
|
||||
myPhotos(filter: Filter): [Photo!]!
|
||||
"Get photo by id, user must own the photo or be admin"
|
||||
photo(id: Int!): Photo!
|
||||
"List of media owned by the logged in user"
|
||||
myMedia(filter: Filter): [Media!]!
|
||||
"Get media by id, user must own the media or be admin"
|
||||
media(id: Int!): Media!
|
||||
|
||||
shareToken(token: String!, password: String): ShareToken!
|
||||
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
||||
|
||||
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
|
||||
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -61,22 +61,22 @@ type Mutation {
|
||||
rootPath: String!
|
||||
): AuthorizeResult
|
||||
|
||||
"Scan all users for new photos"
|
||||
"Scan all users for new media"
|
||||
scanAll: ScannerResult! @isAdmin
|
||||
"Scan a single user for new photos"
|
||||
"Scan a single user for new media"
|
||||
scanUser(userId: Int!): ScannerResult!
|
||||
|
||||
"Generate share token for album"
|
||||
shareAlbum(albumId: Int!, expire: Time, password: String): ShareToken
|
||||
"Generate share token for photo"
|
||||
sharePhoto(photoId: Int!, expire: Time, password: String): ShareToken
|
||||
"Generate share token for media"
|
||||
shareMedia(mediaId: Int!, expire: Time, password: String): ShareToken
|
||||
"Delete a share token by it's token value"
|
||||
deleteShareToken(token: String!): ShareToken
|
||||
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
|
||||
protectShareToken(token: String!, password: String): ShareToken
|
||||
|
||||
"Mark or unmark a photo as being a favorite"
|
||||
favoritePhoto(photoId: Int!, favorite: Boolean!): Photo
|
||||
"Mark or unmark a media as being a favorite"
|
||||
favoriteMedia(mediaId: Int!, favorite: Boolean!): Media
|
||||
|
||||
updateUser(
|
||||
id: Int!
|
||||
@@ -130,7 +130,7 @@ type ScannerResult {
|
||||
message: String
|
||||
}
|
||||
|
||||
"A token used to publicly access an album or photo"
|
||||
"A token used to publicly access an album or media"
|
||||
type ShareToken {
|
||||
id: Int!
|
||||
token: String!
|
||||
@@ -143,8 +143,8 @@ type ShareToken {
|
||||
|
||||
"The album this token shares"
|
||||
album: Album
|
||||
"The photo this token shares"
|
||||
photo: Photo
|
||||
"The media this token shares"
|
||||
media: Media
|
||||
}
|
||||
|
||||
"General public information about the site"
|
||||
@@ -165,8 +165,8 @@ type User {
|
||||
type Album {
|
||||
id: Int!
|
||||
title: String!
|
||||
"The photos inside this album"
|
||||
photos(filter: Filter): [Photo!]!
|
||||
"The media inside this album"
|
||||
media(filter: Filter): [Media!]!
|
||||
"The albums contained in this album"
|
||||
subAlbums(filter: Filter): [Album!]!
|
||||
"The album witch contains this album"
|
||||
@@ -176,13 +176,13 @@ type Album {
|
||||
"The path on the filesystem of the server, where this album is located"
|
||||
filePath: String!
|
||||
"An image in this album used for previewing this album"
|
||||
thumbnail: Photo
|
||||
thumbnail: Media
|
||||
path: [Album!]!
|
||||
|
||||
shares: [ShareToken]
|
||||
}
|
||||
|
||||
type PhotoURL {
|
||||
type MediaURL {
|
||||
"URL for previewing the image"
|
||||
url: String!
|
||||
"Width of the image in pixels"
|
||||
@@ -191,35 +191,35 @@ type PhotoURL {
|
||||
height: Int!
|
||||
}
|
||||
|
||||
type PhotoDownload {
|
||||
type MediaDownload {
|
||||
title: String!
|
||||
width: Int!
|
||||
height: Int!
|
||||
url: String!
|
||||
}
|
||||
|
||||
type Photo {
|
||||
type Media {
|
||||
id: Int!
|
||||
title: String!
|
||||
"Local filepath for the photo"
|
||||
"Local filepath for the media"
|
||||
path: String!
|
||||
"URL to display the photo in a smaller resolution"
|
||||
thumbnail: PhotoURL!
|
||||
"URL to display the photo in full resolution"
|
||||
highRes: PhotoURL!
|
||||
"The album that holds the photo"
|
||||
"URL to display the media in a smaller resolution"
|
||||
thumbnail: MediaURL!
|
||||
"URL to display the photo in full resolution, will be null for videos"
|
||||
highRes: MediaURL
|
||||
"The album that holds the media"
|
||||
album: Album!
|
||||
exif: PhotoEXIF
|
||||
exif: MediaEXIF
|
||||
favorite: Boolean!
|
||||
|
||||
shares: [ShareToken!]!
|
||||
downloads: [PhotoDownload!]!
|
||||
downloads: [MediaDownload!]!
|
||||
}
|
||||
|
||||
"EXIF metadata from the camera"
|
||||
type PhotoEXIF {
|
||||
type MediaEXIF {
|
||||
id: Int!
|
||||
photo: Photo
|
||||
media: Media!
|
||||
"The model name of the camera"
|
||||
camera: String
|
||||
"The maker of the camera"
|
||||
@@ -244,5 +244,5 @@ type PhotoEXIF {
|
||||
type SearchResult {
|
||||
query: String!
|
||||
albums: [Album!]!
|
||||
photos: [Photo!]!
|
||||
media: [Media!]!
|
||||
}
|
||||
|
||||
@@ -21,22 +21,22 @@ import (
|
||||
func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
|
||||
router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||
image_name := mux.Vars(r)["name"]
|
||||
media_name := mux.Vars(r)["name"]
|
||||
|
||||
row := db.QueryRow("SELECT photo_url.purpose, photo_url.content_type, photo_url.photo_id FROM photo_url, photo WHERE photo_url.photo_name = ? AND photo_url.photo_id = photo.photo_id", image_name)
|
||||
row := db.QueryRow("SELECT media_url.purpose, media_url.content_type, media_url.media_id FROM media_url, media WHERE media_url.media_name = ? AND media_url.media_id = media.media_id", media_name)
|
||||
|
||||
var purpose models.MediaPurpose
|
||||
var content_type string
|
||||
var photo_id int
|
||||
var media_id int
|
||||
|
||||
if err := row.Scan(&purpose, &content_type, &photo_id); err != nil {
|
||||
if err := row.Scan(&purpose, &content_type, &media_id); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("404"))
|
||||
return
|
||||
}
|
||||
|
||||
row = db.QueryRow("SELECT * FROM photo WHERE photo_id = ?", photo_id)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
row = db.QueryRow("SELECT * FROM media WHERE media_id = ?", media_id)
|
||||
media, err := models.NewMediaFromRow(row)
|
||||
if err != nil {
|
||||
log.Printf("WARN: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -45,7 +45,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user != nil {
|
||||
row := db.QueryRow("SELECT owner_id FROM album WHERE album.album_id = ?", photo.AlbumId)
|
||||
row := db.QueryRow("SELECT owner_id FROM album WHERE album.album_id = ?", media.AlbumId)
|
||||
var owner_id int
|
||||
|
||||
if err := row.Scan(&owner_id); err != nil {
|
||||
@@ -96,7 +96,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
}
|
||||
}
|
||||
|
||||
if shareToken.AlbumID != nil && photo.AlbumId != *shareToken.AlbumID {
|
||||
if shareToken.AlbumID != nil && media.AlbumId != *shareToken.AlbumID {
|
||||
// Check child albums
|
||||
row := db.QueryRow(`
|
||||
WITH recursive child_albums AS (
|
||||
@@ -105,7 +105,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
SELECT child.* FROM album child JOIN child_albums parent ON parent.album_id = child.parent_album
|
||||
)
|
||||
SELECT * FROM child_albums WHERE album_id = ?
|
||||
`, *shareToken.AlbumID, photo.AlbumId)
|
||||
`, *shareToken.AlbumID, media.AlbumId)
|
||||
|
||||
_, err := models.NewAlbumFromRow(row)
|
||||
if err != nil {
|
||||
@@ -121,7 +121,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
}
|
||||
}
|
||||
|
||||
if shareToken.PhotoID != nil && photo_id != *shareToken.PhotoID {
|
||||
if shareToken.MediaID != nil && media_id != *shareToken.MediaID {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("unauthorized"))
|
||||
return
|
||||
@@ -133,11 +133,11 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
var file *os.File = nil
|
||||
|
||||
if purpose == models.PhotoThumbnail || purpose == models.PhotoHighRes {
|
||||
cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(photo.AlbumId), strconv.Itoa(photo_id), image_name)
|
||||
cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(media.AlbumId), strconv.Itoa(media_id), media_name)
|
||||
}
|
||||
|
||||
if purpose == models.MediaOriginal {
|
||||
cachedPath = photo.Path
|
||||
cachedPath = media.Path
|
||||
}
|
||||
|
||||
file, err = os.Open(cachedPath)
|
||||
@@ -151,7 +151,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = scanner.ProcessMedia(tx, photo)
|
||||
_, err = scanner.ProcessMedia(tx, media)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: processing image not found in cache: %s\n", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (dimensions *PhotoDimensions) ThumbnailScale() PhotoDimensions {
|
||||
|
||||
// EncodeImageData is used to easily decode image data, with a cache so expensive operations are not repeated
|
||||
type EncodeImageData struct {
|
||||
photo *models.Photo
|
||||
media *models.Media
|
||||
_photoImage image.Image
|
||||
_thumbnailImage image.Image
|
||||
_contentType *MediaType
|
||||
@@ -90,7 +90,7 @@ func (img *EncodeImageData) ContentType() (*MediaType, error) {
|
||||
return img._contentType, nil
|
||||
}
|
||||
|
||||
imgType, err := getImageType(img.photo.Path)
|
||||
imgType, err := getImageType(img.media.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (img *EncodeImageData) EncodeHighRes(tx *sql.Tx, outputPath string) error {
|
||||
|
||||
if contentType.isRaw() {
|
||||
if DarktableCli.IsInstalled() {
|
||||
err := DarktableCli.EncodeJpeg(img.photo.Path, outputPath, 70)
|
||||
err := DarktableCli.EncodeJpeg(img.media.Path, outputPath, 70)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func (img *EncodeImageData) photoImage(tx *sql.Tx) (image.Image, error) {
|
||||
return img._photoImage, nil
|
||||
}
|
||||
|
||||
photoFile, err := os.Open(img.photo.Path)
|
||||
photoFile, err := os.Open(img.media.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (img *EncodeImageData) photoImage(tx *sql.Tx) (image.Image, error) {
|
||||
}
|
||||
|
||||
// Get orientation from exif data
|
||||
row := tx.QueryRow("SELECT photo_exif.orientation FROM photo JOIN photo_exif WHERE photo.exif_id = photo_exif.exif_id AND photo.photo_id = ?", img.photo.PhotoID)
|
||||
row := tx.QueryRow("SELECT media_exif.orientation FROM media JOIN media_exif WHERE media.exif_id = media_exif.exif_id AND media.media_id = ?", img.media.MediaID)
|
||||
var orientation *int
|
||||
if err = row.Scan(&orientation); err != nil {
|
||||
// If not found use default orientation (not rotate)
|
||||
|
||||
@@ -14,19 +14,19 @@ import (
|
||||
"github.com/xor-gate/goexif2/mknote"
|
||||
)
|
||||
|
||||
func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, returnErr error) {
|
||||
func ScanEXIF(tx *sql.Tx, media *models.Media) (returnExif *models.MediaEXIF, returnErr error) {
|
||||
|
||||
log.Printf("Scanning for EXIF")
|
||||
|
||||
{
|
||||
// Check if EXIF data already exists
|
||||
if photo.ExifId != nil {
|
||||
row := tx.QueryRow("SELECT * FROM photo_exif WHERE exif_id = ?", photo.ExifId)
|
||||
return models.NewPhotoExifFromRow(row)
|
||||
if media.ExifId != nil {
|
||||
row := tx.QueryRow("SELECT * FROM media_exif WHERE exif_id = ?", media.ExifId)
|
||||
return models.NewMediaExifFromRow(row)
|
||||
}
|
||||
|
||||
row := tx.QueryRow("SELECT photo_exif.* FROM photo, photo_exif WHERE photo.exif_id = photo_exif.exif_id AND photo.photo_id = ?", photo.PhotoID)
|
||||
exifData, err := models.NewPhotoExifFromRow(row)
|
||||
row := tx.QueryRow("SELECT media_exif.* FROM media, media_exif WHERE media.exif_id = media_exif.exif_id AND media.media_id = ?", media.MediaID)
|
||||
exifData, err := models.NewMediaExifFromRow(row)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
} else if exifData != nil {
|
||||
@@ -34,7 +34,7 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
}
|
||||
}
|
||||
|
||||
photoFile, err := os.Open(photo.Path)
|
||||
photoFile, err := os.Open(media.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,24 +54,22 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
return nil, errors.Wrap(err, "Could not decode EXIF")
|
||||
}
|
||||
|
||||
// log.Printf("EXIF DATA FOR %s\n%s\n", photo.Title, exifTags.String())
|
||||
|
||||
valueNames := make([]string, 0)
|
||||
exifValues := make([]interface{}, 0)
|
||||
|
||||
model, err := readStringTag(exifTags, exif.Model, photo)
|
||||
model, err := readStringTag(exifTags, exif.Model, media)
|
||||
if err == nil {
|
||||
valueNames = append(valueNames, "camera")
|
||||
exifValues = append(exifValues, model)
|
||||
}
|
||||
|
||||
maker, err := readStringTag(exifTags, exif.Make, photo)
|
||||
maker, err := readStringTag(exifTags, exif.Make, media)
|
||||
if err == nil {
|
||||
valueNames = append(valueNames, "maker")
|
||||
exifValues = append(exifValues, maker)
|
||||
}
|
||||
|
||||
lens, err := readStringTag(exifTags, exif.LensModel, photo)
|
||||
lens, err := readStringTag(exifTags, exif.LensModel, media)
|
||||
if err == nil {
|
||||
valueNames = append(valueNames, "lens")
|
||||
exifValues = append(exifValues, lens)
|
||||
@@ -83,13 +81,13 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
exifValues = append(exifValues, date)
|
||||
}
|
||||
|
||||
exposure, err := readRationalTag(exifTags, exif.ExposureTime, photo)
|
||||
exposure, err := readRationalTag(exifTags, exif.ExposureTime, media)
|
||||
if err == nil {
|
||||
valueNames = append(valueNames, "exposure")
|
||||
exifValues = append(exifValues, exposure.RatString())
|
||||
}
|
||||
|
||||
apertureRat, err := readRationalTag(exifTags, exif.FNumber, photo)
|
||||
apertureRat, err := readRationalTag(exifTags, exif.FNumber, media)
|
||||
if err == nil {
|
||||
aperture, _ := apertureRat.Float32()
|
||||
valueNames = append(valueNames, "aperture")
|
||||
@@ -98,11 +96,11 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
|
||||
isoTag, err := exifTags.Get(exif.ISOSpeedRatings)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not read ISOSpeedRatings from EXIF: %s\n", photo.Title)
|
||||
log.Printf("WARN: Could not read ISOSpeedRatings from EXIF: %s\n", media.Title)
|
||||
} else {
|
||||
iso, err := isoTag.Int(0)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse EXIF ISOSpeedRatings as integer: %s\n", photo.Title)
|
||||
log.Printf("WARN: Could not parse EXIF ISOSpeedRatings as integer: %s\n", media.Title)
|
||||
} else {
|
||||
valueNames = append(valueNames, "iso")
|
||||
exifValues = append(exifValues, iso)
|
||||
@@ -123,7 +121,7 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
if err == nil {
|
||||
focalLength, err := focalLengthTag.Int(1)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse EXIF FocalLength as rational or integer: %s\n%s\n", photo.Title, err)
|
||||
log.Printf("WARN: Could not parse EXIF FocalLength as rational or integer: %s\n%s\n", media.Title, err)
|
||||
} else {
|
||||
valueNames = append(valueNames, "focal_length")
|
||||
exifValues = append(exifValues, focalLength)
|
||||
@@ -138,13 +136,13 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
exifValues = append(exifValues, flash)
|
||||
}
|
||||
|
||||
orientation, err := readIntegerTag(exifTags, exif.Orientation, photo)
|
||||
orientation, err := readIntegerTag(exifTags, exif.Orientation, media)
|
||||
if err == nil {
|
||||
valueNames = append(valueNames, "orientation")
|
||||
exifValues = append(exifValues, *orientation)
|
||||
}
|
||||
|
||||
exposureProgram, err := readIntegerTag(exifTags, exif.ExposureProgram, photo)
|
||||
exposureProgram, err := readIntegerTag(exifTags, exif.ExposureProgram, media)
|
||||
if err == nil {
|
||||
valueNames = append(valueNames, "exposure_program")
|
||||
exifValues = append(exifValues, *exposureProgram)
|
||||
@@ -167,7 +165,7 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
columns = columns[0 : len(columns)-1]
|
||||
|
||||
// Insert into database
|
||||
result, err := tx.Exec("INSERT INTO photo_exif ("+columns+") VALUES ("+prepareQuestions+")", exifValues...)
|
||||
result, err := tx.Exec("INSERT INTO media_exif ("+columns+") VALUES ("+prepareQuestions+")", exifValues...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -177,85 +175,79 @@ func ScanEXIF(tx *sql.Tx, photo *models.Photo) (returnExif *models.PhotoEXIF, re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Link exif to photo in database
|
||||
result, err = tx.Exec("UPDATE photo SET exif_id = ? WHERE photo_id = ?", exifID, photo.PhotoID)
|
||||
// Link exif to media in database
|
||||
result, err = tx.Exec("UPDATE media SET exif_id = ? WHERE media_id = ?", exifID, media.MediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "linking exif to media in database failed")
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, errors.New("Linking exif to photo in database failed: 0 rows affected")
|
||||
return nil, errors.New("linking exif to media in database failed: 0 rows affected")
|
||||
}
|
||||
|
||||
// Return newly created exif row
|
||||
row := tx.QueryRow("SELECT * FROM photo_exif WHERE exif_id = ?", exifID)
|
||||
return models.NewPhotoExifFromRow(row)
|
||||
row := tx.QueryRow("SELECT * FROM media_exif WHERE exif_id = ?", exifID)
|
||||
return models.NewMediaExifFromRow(row)
|
||||
}
|
||||
|
||||
func readStringTag(tags *exif.Exif, name exif.FieldName, photo *models.Photo) (*string, error) {
|
||||
func readStringTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*string, error) {
|
||||
tag, err := tags.Get(name)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not read %s from EXIF: %s\n", name, photo.Title)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title)
|
||||
}
|
||||
|
||||
if tag != nil {
|
||||
value, err := tag.StringVal()
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse %s from EXIF as string: %s\n", name, photo.Title)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "could not parse %s from EXIF as string: %s", name, media.Title)
|
||||
}
|
||||
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
log.Printf("WARN: EXIF tag %s returned null: %s\n", name, photo.Title)
|
||||
log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title)
|
||||
return nil, errors.New("exif tag returned null")
|
||||
}
|
||||
|
||||
func readRationalTag(tags *exif.Exif, name exif.FieldName, photo *models.Photo) (*big.Rat, error) {
|
||||
func readRationalTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*big.Rat, error) {
|
||||
tag, err := tags.Get(name)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not read %s from EXIF: %s\n", name, photo.Title)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title)
|
||||
}
|
||||
|
||||
if tag != nil {
|
||||
value, err := tag.Rat(0)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse %s from EXIF as rational: %s\n%s\n", name, photo.Title, err)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "could not parse %s from EXIF as rational: %s", name, media.Title)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
log.Printf("WARN: EXIF tag %s returned null: %s\n", name, photo.Title)
|
||||
log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title)
|
||||
return nil, errors.New("exif tag returned null")
|
||||
}
|
||||
|
||||
func readIntegerTag(tags *exif.Exif, name exif.FieldName, photo *models.Photo) (*int, error) {
|
||||
func readIntegerTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*int, error) {
|
||||
tag, err := tags.Get(name)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not read %s from EXIF: %s\n", name, photo.Title)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title)
|
||||
}
|
||||
|
||||
if tag != nil {
|
||||
value, err := tag.Int(0)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Could not parse %s from EXIF as integer: %s\n%s\n", name, photo.Title, err)
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "Could not parse %s from EXIF as integer: %s", name, media.Title)
|
||||
}
|
||||
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
log.Printf("WARN: EXIF tag %s returned null: %s\n", name, photo.Title)
|
||||
log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title)
|
||||
return nil, errors.New("exif tag returned null")
|
||||
}
|
||||
|
||||
@@ -22,16 +22,16 @@ import (
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// Higher order function used to check if PhotoURL for a given PhotoPurpose exists
|
||||
func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.MediaPurpose) (*models.PhotoURL, error), error) {
|
||||
photoURLExistsStmt, err := tx.Prepare("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?")
|
||||
// Higher order function used to check if MediaURL for a given MediaPurpose exists
|
||||
func makePhotoURLChecker(tx *sql.Tx, mediaID int) (func(purpose models.MediaPurpose) (*models.MediaURL, error), error) {
|
||||
mediaURLExistsStmt, err := tx.Prepare("SELECT * FROM media_url WHERE media_id = ? AND purpose = ?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(purpose models.MediaPurpose) (*models.PhotoURL, error) {
|
||||
row := photoURLExistsStmt.QueryRow(photoID, purpose)
|
||||
photoURL, err := models.NewPhotoURLFromRow(row)
|
||||
return func(purpose models.MediaPurpose) (*models.MediaURL, error) {
|
||||
row := mediaURLExistsStmt.QueryRow(mediaID, purpose)
|
||||
mediaURL, err := models.NewMediaURLFromRow(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -39,22 +39,22 @@ func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.MediaPurp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return photoURL, nil
|
||||
return mediaURL, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ProcessMedia(tx *sql.Tx, photo *models.Photo) (bool, error) {
|
||||
func ProcessMedia(tx *sql.Tx, media *models.Media) (bool, error) {
|
||||
imageData := EncodeImageData{
|
||||
photo: photo,
|
||||
media: media,
|
||||
}
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "get content-type of media (%s)", photo.Path)
|
||||
return false, errors.Wrapf(err, "get content-type of media (%s)", media.Path)
|
||||
}
|
||||
|
||||
// Make sure photo cache directory exists
|
||||
mediaCachePath, err := makeMediaCacheDir(photo)
|
||||
// Make sure media cache directory exists
|
||||
mediaCachePath, err := makeMediaCacheDir(media)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "cache directory error")
|
||||
}
|
||||
@@ -68,13 +68,13 @@ func ProcessMedia(tx *sql.Tx, photo *models.Photo) (bool, error) {
|
||||
|
||||
func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string) (bool, error) {
|
||||
|
||||
photo := imageData.photo
|
||||
photo := imageData.media
|
||||
|
||||
log.Printf("Processing photo: %s\n", photo.Path)
|
||||
|
||||
didProcess := false
|
||||
|
||||
photoUrlFromDB, err := makePhotoURLChecker(tx, photo.PhotoID)
|
||||
photoUrlFromDB, err := makePhotoURLChecker(tx, photo.MediaID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -128,19 +128,19 @@ func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
photo.PhotoID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
|
||||
_, 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")
|
||||
if err != nil {
|
||||
log.Printf("Could not insert highres photo url: %d, %s\n", photo.PhotoID, path.Base(photo.Path))
|
||||
log.Printf("Could not insert highres media url: %d, %s\n", photo.MediaID, path.Base(photo.Path))
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Verify that highres photo still exists in cache
|
||||
baseImagePath = path.Join(*photoCachePath, highResURL.PhotoName)
|
||||
baseImagePath = path.Join(*photoCachePath, highResURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(baseImagePath); os.IsNotExist(err) {
|
||||
fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.PhotoName)
|
||||
fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.MediaName)
|
||||
didProcess = true
|
||||
|
||||
err = imageData.EncodeHighRes(tx, baseImagePath)
|
||||
@@ -188,17 +188,17 @@ func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string
|
||||
return false, errors.Wrap(err, "could not create thumbnail cached image")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg")
|
||||
_, 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")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
// Verify that thumbnail photo still exists in cache
|
||||
thumbPath := path.Join(*photoCachePath, thumbURL.PhotoName)
|
||||
thumbPath := path.Join(*photoCachePath, thumbURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
|
||||
didProcess = true
|
||||
fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.PhotoName)
|
||||
fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.MediaName)
|
||||
|
||||
_, err := EncodeThumbnail(baseImagePath, thumbPath)
|
||||
if err != nil {
|
||||
@@ -210,7 +210,7 @@ func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string
|
||||
return didProcess, nil
|
||||
}
|
||||
|
||||
func makeMediaCacheDir(photo *models.Photo) (*string, error) {
|
||||
func makeMediaCacheDir(photo *models.Media) (*string, error) {
|
||||
|
||||
// Make root cache dir if not exists
|
||||
if _, err := os.Stat(PhotoCache()); os.IsNotExist(err) {
|
||||
@@ -228,7 +228,7 @@ func makeMediaCacheDir(photo *models.Photo) (*string, error) {
|
||||
}
|
||||
|
||||
// Make photo cache dir if not exists
|
||||
photoCachePath := path.Join(albumCachePath, strconv.Itoa(photo.PhotoID))
|
||||
photoCachePath := path.Join(albumCachePath, strconv.Itoa(photo.MediaID))
|
||||
if _, err := os.Stat(photoCachePath); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil {
|
||||
return nil, errors.Wrap(err, "could not make photo image cache directory")
|
||||
@@ -238,7 +238,7 @@ func makeMediaCacheDir(photo *models.Photo) (*string, error) {
|
||||
return &photoCachePath, nil
|
||||
}
|
||||
|
||||
func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData *EncodeImageData, photoDimensions *PhotoDimensions) error {
|
||||
func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeImageData, photoDimensions *PhotoDimensions) error {
|
||||
photoName := path.Base(photo.Path)
|
||||
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
|
||||
photoBaseExt := path.Ext(photoName)
|
||||
@@ -251,9 +251,9 @@ func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData *EncodeIma
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoDimensions.Width, photoDimensions.Height, models.MediaOriginal, contentType)
|
||||
_, 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)
|
||||
if err != nil {
|
||||
log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
|
||||
log.Printf("Could not insert original photo url: %d, %s\n", photo.MediaID, photoName)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
)
|
||||
|
||||
func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string) (bool, error) {
|
||||
video := imageData.photo
|
||||
video := imageData.media
|
||||
didProcess := false
|
||||
|
||||
log.Printf("Processing video: %s", video.Path)
|
||||
|
||||
mediaUrlFromDB, err := makePhotoURLChecker(tx, video.PhotoID)
|
||||
mediaUrlFromDB, err := makePhotoURLChecker(tx, video.MediaID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB) {
|
||||
notifyThrottle.Trigger(nil)
|
||||
|
||||
// Scan for photos
|
||||
albumPhotos, err := findPhotosForAlbum(album, cache, db, func(photo *models.Photo, newPhoto bool) {
|
||||
albumPhotos, err := findPhotosForAlbum(album, cache, db, func(photo *models.Media, newPhoto bool) {
|
||||
if newPhoto {
|
||||
notifyThrottle.Trigger(func() {
|
||||
notification.BroadcastNotification(&models.Notification{
|
||||
@@ -80,9 +80,9 @@ func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB) {
|
||||
}
|
||||
}
|
||||
|
||||
func findPhotosForAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB, onScanPhoto func(photo *models.Photo, newPhoto bool)) ([]*models.Photo, error) {
|
||||
func findPhotosForAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB, onScanPhoto func(photo *models.Media, newPhoto bool)) ([]*models.Media, error) {
|
||||
|
||||
albumPhotos := make([]*models.Photo, 0)
|
||||
albumPhotos := make([]*models.Media, 0)
|
||||
|
||||
dirContent, err := ioutil.ReadDir(album.Path)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
)
|
||||
|
||||
func ScanPhoto(tx *sql.Tx, photoPath string, albumId int) (*models.Photo, bool, error) {
|
||||
func ScanPhoto(tx *sql.Tx, photoPath string, albumId int) (*models.Media, bool, error) {
|
||||
photoName := path.Base(photoPath)
|
||||
|
||||
// Check if image already exists
|
||||
{
|
||||
row := tx.QueryRow("SELECT * FROM photo WHERE path_hash = MD5(?)", photoPath)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
photo, err := models.NewMediaFromRow(row)
|
||||
if err != sql.ErrNoRows {
|
||||
if err == nil {
|
||||
log.Printf("Image already scanned: %s\n", photoPath)
|
||||
@@ -32,13 +32,13 @@ func ScanPhoto(tx *sql.Tx, photoPath string, albumId int) (*models.Photo, bool,
|
||||
log.Printf("ERROR: Could not insert photo into database")
|
||||
return nil, false, err
|
||||
}
|
||||
photo_id, err := result.LastInsertId()
|
||||
media_id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
row := tx.QueryRow("SELECT * FROM photo WHERE photo_id = ?", photo_id)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
row := tx.QueryRow("SELECT * FROM photo WHERE media_id = ?", media_id)
|
||||
photo, err := models.NewMediaFromRow(row)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -209,64 +209,6 @@ func deleteOldUserAlbums(db *sql.DB, scannedAlbums []*models.Album, user *models
|
||||
return deleteErrors
|
||||
}
|
||||
|
||||
// func cleanupCache(database *sql.DB, cache *ScannerCache, user *models.User) {
|
||||
|
||||
// // Delete old photos
|
||||
// photo_args := make([]interface{}, 0)
|
||||
// photo_args = append(photo_args, user.UserID)
|
||||
// photo_args = append(photo_args, cache.photo_paths_scanned...)
|
||||
|
||||
// photo_questions := strings.Repeat("MD5(?),", len(cache.photo_paths_scanned))[:len(cache.photo_paths_scanned)*7-1]
|
||||
|
||||
// rows, err = database.Query(`
|
||||
// SELECT photo.photo_id as photo_id, album.album_id as album_id FROM photo JOIN album ON photo.album_id = album.album_id
|
||||
// WHERE album.owner_id = ? AND photo.path_hash NOT IN (`+photo_questions+`)
|
||||
// `, photo_args...)
|
||||
// if err != nil {
|
||||
// ScannerError("Could not get deleted photos from database: %s\n", err)
|
||||
// return
|
||||
// }
|
||||
// defer rows.Close()
|
||||
|
||||
// deleted_photo_ids := make([]interface{}, 0)
|
||||
|
||||
// for rows.Next() {
|
||||
// var photo_id int
|
||||
// var album_id int
|
||||
|
||||
// if err := rows.Scan(&photo_id, &album_id); err != nil {
|
||||
// ScannerError("Could not parse photo to be removed (album_id %d, photo_id %d): %s\n", album_id, photo_id, err)
|
||||
// }
|
||||
|
||||
// deleted_photo_ids = append(deleted_photo_ids, photo_id)
|
||||
// cache_path := path.Join("./photo_cache", strconv.Itoa(album_id), strconv.Itoa(photo_id))
|
||||
// err := os.RemoveAll(cache_path)
|
||||
// if err != nil {
|
||||
// ScannerError("Could not delete unused cache photo folder: %s\n%s\n", cache_path, err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if len(deleted_photo_ids) > 0 {
|
||||
// photo_questions = strings.Repeat("?,", len(deleted_photo_ids))[:len(deleted_photo_ids)*2-1]
|
||||
|
||||
// if _, err := database.Exec("DELETE FROM photo WHERE photo_id IN ("+photo_questions+")", deleted_photo_ids...); err != nil {
|
||||
// ScannerError("Could not delete old photos from database:\n%s\n", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if len(deleted_album_ids) > 0 || len(deleted_photo_ids) > 0 {
|
||||
// timeout := 3000
|
||||
// notification.BroadcastNotification(&models.Notification{
|
||||
// Key: utils.GenerateToken(),
|
||||
// Type: models.NotificationTypeMessage,
|
||||
// Header: "Deleted old photos",
|
||||
// Content: fmt.Sprintf("Deleted %d albums and %d photos, that was not found on disk", len(deleted_album_ids), len(deleted_photo_ids)),
|
||||
// Timeout: &timeout,
|
||||
// })
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
func ScannerError(format string, args ...interface{}) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ const photoQuery = gql`
|
||||
myAlbums(filter: { order_by: "title", order_direction: ASC, limit: 100 }) {
|
||||
title
|
||||
id
|
||||
photos(
|
||||
filter: { order_by: "photo.title", order_direction: DESC, limit: 12 }
|
||||
media(
|
||||
filter: { order_by: "media.title", order_direction: DESC, limit: 12 }
|
||||
) {
|
||||
id
|
||||
title
|
||||
@@ -66,7 +66,7 @@ class PhotosPage extends Component {
|
||||
}
|
||||
|
||||
nextImage() {
|
||||
const albumImageCount = this.albums[this.state.activeAlbumIndex].photos
|
||||
const albumImageCount = this.albums[this.state.activeAlbumIndex].media
|
||||
.length
|
||||
|
||||
if (this.state.activePhotoIndex + 1 < albumImageCount) {
|
||||
@@ -115,7 +115,7 @@ class PhotosPage extends Component {
|
||||
this.setPresenting(presenting, index)
|
||||
}
|
||||
loading={loading}
|
||||
photos={album.photos}
|
||||
media={album.media}
|
||||
nextImage={this.nextImage}
|
||||
previousImage={this.previousImage}
|
||||
/>
|
||||
@@ -126,7 +126,7 @@ class PhotosPage extends Component {
|
||||
let activeImage = null
|
||||
if (this.state.activeAlbumIndex != -1) {
|
||||
activeImage =
|
||||
data.myAlbums[this.state.activeAlbumIndex].photos[
|
||||
data.myAlbums[this.state.activeAlbumIndex].media[
|
||||
this.state.activePhotoIndex
|
||||
].id
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const SEARCH_QUERY = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
photos {
|
||||
media {
|
||||
id
|
||||
title
|
||||
thumbnail {
|
||||
@@ -128,20 +128,20 @@ const SearchResults = ({ result }) => {
|
||||
const { data, loading } = result
|
||||
const query = data && data.search.query
|
||||
|
||||
const photos = (data && data.search.photos) || []
|
||||
const media = (data && data.search.media) || []
|
||||
const albums = (data && data.search.albums) || []
|
||||
|
||||
let message = null
|
||||
if (loading) message = 'Loading results...'
|
||||
else if (data && photos.length == 0 && albums.length == 0)
|
||||
else if (data && media.length == 0 && albums.length == 0)
|
||||
message = 'No results found'
|
||||
|
||||
const albumElements = albums.map(album => (
|
||||
<AlbumRow key={album.id} query={query} album={album} />
|
||||
))
|
||||
|
||||
const photoElements = photos.map(photo => (
|
||||
<PhotoRow key={photo.id} query={query} photo={photo} />
|
||||
const mediaElements = media.map(media => (
|
||||
<PhotoRow key={media.id} query={query} photo={media} />
|
||||
))
|
||||
|
||||
return (
|
||||
@@ -155,8 +155,8 @@ const SearchResults = ({ result }) => {
|
||||
{message}
|
||||
{albumElements.length > 0 && <ResultTitle>Albums</ResultTitle>}
|
||||
{albumElements}
|
||||
{photoElements.length > 0 && <ResultTitle>Photos</ResultTitle>}
|
||||
{photoElements}
|
||||
{mediaElements.length > 0 && <ResultTitle>Photos</ResultTitle>}
|
||||
{mediaElements}
|
||||
</Results>
|
||||
)
|
||||
}
|
||||
@@ -192,16 +192,16 @@ const RowTitle = styled.span`
|
||||
padding-left: 8px;
|
||||
`
|
||||
|
||||
const PhotoRow = ({ query, photo }) => (
|
||||
<RowLink to={`/album/${photo.album.id}`}>
|
||||
<PhotoSearchThumbnail src={photo.thumbnail.url} />
|
||||
<RowTitle>{searchHighlighted(query, photo.title)}</RowTitle>
|
||||
const PhotoRow = ({ query, media }) => (
|
||||
<RowLink to={`/album/${media.album.id}`}>
|
||||
<PhotoSearchThumbnail src={media.thumbnail.url} />
|
||||
<RowTitle>{searchHighlighted(query, media.title)}</RowTitle>
|
||||
</RowLink>
|
||||
)
|
||||
|
||||
PhotoRow.propTypes = {
|
||||
query: PropTypes.string.isRequired,
|
||||
photo: PropTypes.object.isRequired,
|
||||
media: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
const AlbumRow = ({ query, album }) => (
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Icon } from 'semantic-ui-react'
|
||||
import ProtectedImage from './ProtectedImage'
|
||||
|
||||
const markFavoriteMutation = gql`
|
||||
mutation markPhotoFavorite($photoId: Int!, $favorite: Boolean!) {
|
||||
favoritePhoto(photoId: $photoId, favorite: $favorite) {
|
||||
mutation markMediaFavorite($mediaId: Int!, $favorite: Boolean!) {
|
||||
favoriteMedia(mediaId: $mediaId, favorite: $favorite) {
|
||||
id
|
||||
favorite
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export const Photo = ({
|
||||
event.stopPropagation()
|
||||
markFavorite({
|
||||
variables: {
|
||||
photoId: photo.id,
|
||||
mediaId: photo.id,
|
||||
favorite: !photo.favorite,
|
||||
},
|
||||
optimisticResponse: {
|
||||
|
||||
@@ -23,7 +23,7 @@ const PhotoFiller = styled.div`
|
||||
|
||||
const PhotoGallery = ({
|
||||
activeIndex = -1,
|
||||
photos,
|
||||
media,
|
||||
loading,
|
||||
onSelectImage,
|
||||
presenting,
|
||||
@@ -57,14 +57,14 @@ const PhotoGallery = ({
|
||||
}
|
||||
})
|
||||
|
||||
const activeImage = photos && activeIndex != -1 && photos[activeIndex]
|
||||
const activeImage = media && activeIndex != -1 && media[activeIndex]
|
||||
|
||||
const getPhotoElements = updateSidebar => {
|
||||
let photoElements = []
|
||||
if (photos) {
|
||||
photos.filter(photo => photo.thumbnail)
|
||||
if (media) {
|
||||
media.filter(media => media.thumbnail)
|
||||
|
||||
photoElements = photos.map((photo, index) => {
|
||||
photoElements = media.map((photo, index) => {
|
||||
const active = activeIndex == index
|
||||
|
||||
let minWidth = 100
|
||||
@@ -121,7 +121,7 @@ const PhotoGallery = ({
|
||||
|
||||
PhotoGallery.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
photos: PropTypes.array,
|
||||
media: PropTypes.array,
|
||||
activeIndex: PropTypes.number,
|
||||
presenting: PropTypes.bool,
|
||||
onSelectImage: PropTypes.func,
|
||||
|
||||
Reference in New Issue
Block a user