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:
viktorstrate
2020-07-10 14:26:19 +02:00
parent 3564866f41
commit df80802cab
27 changed files with 2145 additions and 2345 deletions

View 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;

View 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;

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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]
}

View File

@@ -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!]!
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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...)

View File

@@ -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
}

View File

@@ -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 }) => (

View File

@@ -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: {

View File

@@ -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,