From e66a9199a1a5602a06d4fa5aff9997f5ae685511 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Mon, 27 Sep 2021 20:23:10 +0200
Subject: [PATCH 1/5] Add timeline resolver tests

---
 .../models/actions/timeline_actions.go        |  36 ++++++
 .../models/actions/timeline_actions_test.go   | 112 ++++++++++++++++++
 api/graphql/models/user.go                    |  21 ++++
 api/graphql/resolvers/media.go                |  18 +--
 api/graphql/resolvers/timeline.go             |  27 +----
 5 files changed, 172 insertions(+), 42 deletions(-)
 create mode 100644 api/graphql/models/actions/timeline_actions.go
 create mode 100644 api/graphql/models/actions/timeline_actions_test.go

diff --git a/api/graphql/models/actions/timeline_actions.go b/api/graphql/models/actions/timeline_actions.go
new file mode 100644
index 0000000..d80a1c6
--- /dev/null
+++ b/api/graphql/models/actions/timeline_actions.go
@@ -0,0 +1,36 @@
+package actions
+
+import (
+	"time"
+
+	"github.com/photoview/photoview/api/graphql/models"
+	"gorm.io/gorm"
+)
+
+func MyTimeline(db *gorm.DB, user *models.User, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
+	query := db.
+		Joins("JOIN albums ON media.album_id = albums.id").
+		Where("albums.id IN (?)", db.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID)).
+		Order("YEAR(media.date_shot) DESC").
+		Order("MONTH(media.date_shot) DESC").
+		Order("DAY(media.date_shot) DESC").
+		Order("albums.title ASC").
+		Order("TIME(media.date_shot) DESC")
+
+	if fromDate != nil {
+		query = query.Where("media.date_shot < ?", fromDate)
+	}
+
+	if onlyFavorites != nil && *onlyFavorites == true {
+		query = query.Where("media.id IN (?)", db.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
+	}
+
+	query = models.FormatSQL(query, nil, paginate)
+
+	var media []*models.Media
+	if err := query.Find(&media).Error; err != nil {
+		return nil, err
+	}
+
+	return media, nil
+}
diff --git a/api/graphql/models/actions/timeline_actions_test.go b/api/graphql/models/actions/timeline_actions_test.go
new file mode 100644
index 0000000..92fee1c
--- /dev/null
+++ b/api/graphql/models/actions/timeline_actions_test.go
@@ -0,0 +1,112 @@
+package actions_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/photoview/photoview/api/graphql/models"
+	"github.com/photoview/photoview/api/graphql/models/actions"
+	"github.com/photoview/photoview/api/test_utils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMyTimeline(t *testing.T) {
+	db := test_utils.DatabaseTest(t)
+
+	password := "1234"
+	user, err := models.RegisterUser(db, "user", &password, false)
+	assert.NoError(t, err)
+
+	rootAlbum := models.Album{
+		Title: "root",
+		Path:  "/photos",
+	}
+
+	assert.NoError(t, db.Save(&rootAlbum).Error)
+
+	childAlbum := models.Album{
+		Title:         "subalbum",
+		Path:          "/photos/subalbum",
+		ParentAlbumID: &rootAlbum.ID,
+	}
+
+	assert.NoError(t, db.Save(&childAlbum).Error)
+
+	assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
+	assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum))
+
+	media := []models.Media{
+		{
+			Title:    "pic1",
+			Path:     "/photos/pic1",
+			AlbumID:  rootAlbum.ID,
+			DateShot: time.Unix(1632758400, 0), // Sep 27 2021 16:00:00
+		},
+		{
+			Title:    "pic2",
+			Path:     "/photos/pic2",
+			AlbumID:  rootAlbum.ID,
+			DateShot: time.Unix(1628762400, 0), // Aug 12 2021 10:00:00
+		},
+		{
+			Title:    "pic3",
+			Path:     "/photos/subalbum/pic3",
+			AlbumID:  childAlbum.ID,
+			DateShot: time.Unix(1632763800, 0), // Sep 27 2021 17:30:00
+		},
+		{
+			Title:    "pic4",
+			Path:     "/photos/subalbum/pic4",
+			AlbumID:  childAlbum.ID,
+			DateShot: time.Unix(1628775900, 0), // Aug 12 2021 13:45:00
+		},
+	}
+
+	assert.NoError(t, db.Save(&media).Error)
+
+	_, err = user.FavoriteMedia(db, media[0].ID, true)
+	assert.NoError(t, err)
+
+	// Add media not owned by first user
+	anotherUser, err := models.RegisterUser(db, "user2", &password, false)
+	assert.NoError(t, err)
+
+	anotherAlbum := models.Album{
+		Title: "AnotherAlbum",
+		Path:  "/another",
+	}
+
+	assert.NoError(t, db.Save(&anotherAlbum).Error)
+
+	anotherMedia := models.Media{
+		Title:   "anotherPic",
+		Path:    "/another/anotherPic",
+		AlbumID: anotherAlbum.ID,
+	}
+
+	assert.NoError(t, db.Save(&anotherMedia).Error)
+	assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum))
+
+	t.Run("MyTimeline with no filters", func(t *testing.T) {
+		timelineMedia, err := actions.MyTimeline(db, user, nil, nil, nil)
+
+		assert.NoError(t, err)
+		assert.Len(t, timelineMedia, 4)
+	})
+
+	t.Run("MyTimeline with only favorites", func(t *testing.T) {
+		favorites := true
+		timelineMedia, err := actions.MyTimeline(db, user, nil, &favorites, nil)
+
+		assert.NoError(t, err)
+		assert.Len(t, timelineMedia, 1)
+	})
+
+	t.Run("MyTimeline before date", func(t *testing.T) {
+		beforeDate := time.Unix(1629792000, 0) // Aug 24 2021 08:00:00
+		timelineMedia, err := actions.MyTimeline(db, user, nil, nil, &beforeDate)
+
+		assert.NoError(t, err)
+		assert.Len(t, timelineMedia, 2)
+	})
+}
diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go
index 49d021a..854885e 100644
--- a/api/graphql/models/user.go
+++ b/api/graphql/models/user.go
@@ -8,6 +8,7 @@ import (
 	"github.com/pkg/errors"
 	"golang.org/x/crypto/bcrypt"
 	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
 )
 
 type User struct {
@@ -185,3 +186,23 @@ func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) {
 
 	return len(ownedParents) > 0, nil
 }
+
+// FavoriteMedia sets/clears a media as favorite for the user
+func (user *User) FavoriteMedia(db *gorm.DB, mediaID int, favorite bool) (*Media, error) {
+	userMediaData := UserMediaData{
+		UserID:   user.ID,
+		MediaID:  mediaID,
+		Favorite: favorite,
+	}
+
+	if err := db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&userMediaData).Error; err != nil {
+		return nil, errors.Wrapf(err, "update user favorite media in database")
+	}
+
+	var media Media
+	if err := db.First(&media, mediaID).Error; err != nil {
+		return nil, errors.Wrap(err, "get media from database after favorite update")
+	}
+
+	return &media, nil
+}
diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go
index a660175..66c5a71 100644
--- a/api/graphql/resolvers/media.go
+++ b/api/graphql/resolvers/media.go
@@ -11,7 +11,6 @@ import (
 	"github.com/photoview/photoview/api/graphql/models/actions"
 	"github.com/photoview/photoview/api/scanner/face_detection"
 	"github.com/pkg/errors"
-	"gorm.io/gorm/clause"
 )
 
 func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
@@ -198,22 +197,7 @@ func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favor
 		return nil, auth.ErrUnauthorized
 	}
 
-	userMediaData := models.UserMediaData{
-		UserID:   user.ID,
-		MediaID:  mediaID,
-		Favorite: favorite,
-	}
-
-	if err := r.Database.Clauses(clause.OnConflict{UpdateAll: true}).Create(&userMediaData).Error; err != nil {
-		return nil, errors.Wrapf(err, "update user favorite media in database")
-	}
-
-	var media models.Media
-	if err := r.Database.First(&media, mediaID).Error; err != nil {
-		return nil, errors.Wrap(err, "get media from database after favorite update")
-	}
-
-	return &media, nil
+	return user.FavoriteMedia(r.Database, mediaID, favorite)
 }
 
 func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
diff --git a/api/graphql/resolvers/timeline.go b/api/graphql/resolvers/timeline.go
index a26f7dd..aef3f6e 100644
--- a/api/graphql/resolvers/timeline.go
+++ b/api/graphql/resolvers/timeline.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/photoview/photoview/api/graphql/auth"
 	"github.com/photoview/photoview/api/graphql/models"
+	"github.com/photoview/photoview/api/graphql/models/actions"
 )
 
 func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
@@ -14,29 +15,5 @@ func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Paginat
 		return nil, auth.ErrUnauthorized
 	}
 
-	query := r.Database.
-		Joins("JOIN albums ON media.album_id = albums.id").
-		Where("albums.id IN (?)", r.Database.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID)).
-		Order("YEAR(media.date_shot) DESC").
-		Order("MONTH(media.date_shot) DESC").
-		Order("DAY(media.date_shot) DESC").
-		Order("albums.title ASC").
-		Order("TIME(media.date_shot) DESC")
-
-	if fromDate != nil {
-		query = query.Where("media.date_shot < ?", fromDate)
-	}
-
-	if onlyFavorites != nil && *onlyFavorites == true {
-		query = query.Where("media.id IN (?)", r.Database.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
-	}
-
-	query = models.FormatSQL(query, nil, paginate)
-
-	var media []*models.Media
-	if err := query.Find(&media).Error; err != nil {
-		return nil, err
-	}
-
-	return media, nil
+	return actions.MyTimeline(r.Database, user, paginate, onlyFavorites, fromDate)
 }

From c7865a94e45306b5b4a0c7ac6f92a460a48bdf90 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Mon, 27 Sep 2021 20:43:26 +0200
Subject: [PATCH 2/5] Fix problem with postgres for MyTimeline action

---
 .../models/actions/timeline_actions.go        | 28 +++++++++++++------
 .../models/actions/timeline_actions_test.go   |  5 ++++
 2 files changed, 25 insertions(+), 8 deletions(-)

diff --git a/api/graphql/models/actions/timeline_actions.go b/api/graphql/models/actions/timeline_actions.go
index d80a1c6..903b874 100644
--- a/api/graphql/models/actions/timeline_actions.go
+++ b/api/graphql/models/actions/timeline_actions.go
@@ -8,21 +8,33 @@ import (
 )
 
 func MyTimeline(db *gorm.DB, user *models.User, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
+
 	query := db.
 		Joins("JOIN albums ON media.album_id = albums.id").
-		Where("albums.id IN (?)", db.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID)).
-		Order("YEAR(media.date_shot) DESC").
-		Order("MONTH(media.date_shot) DESC").
-		Order("DAY(media.date_shot) DESC").
-		Order("albums.title ASC").
-		Order("TIME(media.date_shot) DESC")
+		Where("albums.id IN (?)", db.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID))
+
+	if db.Dialector.Name() == "postgres" {
+		query = query.
+			Order("DATE_TRUNC('year', date_shot) DESC").
+			Order("DATE_TRUNC('month', date_shot) DESC").
+			Order("DATE_TRUNC('day', date_shot) DESC").
+			Order("albums.title ASC").
+			Order("media.date_shot DESC")
+	} else {
+		query = query.
+			Order("YEAR(media.date_shot) DESC").
+			Order("MONTH(media.date_shot) DESC").
+			Order("DAY(media.date_shot) DESC").
+			Order("albums.title ASC").
+			Order("TIME(media.date_shot) DESC")
+	}
 
 	if fromDate != nil {
 		query = query.Where("media.date_shot < ?", fromDate)
 	}
 
-	if onlyFavorites != nil && *onlyFavorites == true {
-		query = query.Where("media.id IN (?)", db.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
+	if onlyFavorites != nil && *onlyFavorites {
+		query = query.Where("media.id IN (?)", db.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite"))
 	}
 
 	query = models.FormatSQL(query, nil, paginate)
diff --git a/api/graphql/models/actions/timeline_actions_test.go b/api/graphql/models/actions/timeline_actions_test.go
index 92fee1c..9e738b5 100644
--- a/api/graphql/models/actions/timeline_actions_test.go
+++ b/api/graphql/models/actions/timeline_actions_test.go
@@ -92,6 +92,11 @@ func TestMyTimeline(t *testing.T) {
 
 		assert.NoError(t, err)
 		assert.Len(t, timelineMedia, 4)
+
+		for i, title := range []string{"pic1", "pic3", "pic2", "pic4"} {
+			assert.Equalf(t, timelineMedia[i].Title, title, "Element %d didn't match: got %s expected %s", i, timelineMedia[i].Title, title)
+		}
+
 	})
 
 	t.Run("MyTimeline with only favorites", func(t *testing.T) {

From 00e436a518b7b7b88d72b1cac284e54d0d513d5f Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Mon, 27 Sep 2021 20:59:41 +0200
Subject: [PATCH 3/5] Add tests for album path

---
 .github/workflows/tests.yml                   |  1 +
 api/graphql/models/actions/album_actions.go   | 35 +++++++++++++++++++
 .../models/actions/album_actions_test.go      | 30 ++++++++++++++++
 api/graphql/resolvers/album.go                | 33 +----------------
 4 files changed, 67 insertions(+), 32 deletions(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 848aa56..507f3e7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,6 +12,7 @@ jobs:
     runs-on: ubuntu-20.04
 
     strategy:
+      fail-fast: false
       matrix:
         database: ['mysql', 'postgres', 'sqlite']
 
diff --git a/api/graphql/models/actions/album_actions.go b/api/graphql/models/actions/album_actions.go
index 6787249..2c429c8 100644
--- a/api/graphql/models/actions/album_actions.go
+++ b/api/graphql/models/actions/album_actions.go
@@ -72,6 +72,41 @@ func Album(db *gorm.DB, user *models.User, id int) (*models.Album, error) {
 	return &album, nil
 }
 
+func AlbumPath(db *gorm.DB, user *models.User, album *models.Album) ([]*models.Album, error) {
+	var album_path []*models.Album
+
+	err := db.Raw(`
+		WITH recursive path_albums AS (
+			SELECT * FROM albums anchor WHERE anchor.id = ?
+			UNION
+			SELECT parent.* FROM path_albums child JOIN albums parent ON parent.id = child.parent_album_id
+		)
+		SELECT * FROM path_albums WHERE id != ?
+	`, album.ID, album.ID).Scan(&album_path).Error
+
+	// Make sure to only return albums this user owns
+	for i := len(album_path) - 1; i >= 0; i-- {
+		album := album_path[i]
+
+		owns, err := user.OwnsAlbum(db, album)
+		if err != nil {
+			return nil, err
+		}
+
+		if !owns {
+			album_path = album_path[i+1:]
+			break
+		}
+
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return album_path, nil
+}
+
 func SetAlbumCover(db *gorm.DB, user *models.User, mediaID int) (*models.Album, error) {
 	var media models.Media
 
diff --git a/api/graphql/models/actions/album_actions_test.go b/api/graphql/models/actions/album_actions_test.go
index 3cdc4e8..99a76bf 100644
--- a/api/graphql/models/actions/album_actions_test.go
+++ b/api/graphql/models/actions/album_actions_test.go
@@ -9,6 +9,36 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestAlbumPath(t *testing.T) {
+	db := test_utils.DatabaseTest(t)
+
+	album := models.Album{
+		Title: "Three",
+		Path:  "/one/two/three",
+		ParentAlbum: &models.Album{
+			Title: "Two",
+			Path:  "/one/two",
+			ParentAlbum: &models.Album{
+				Title: "One",
+				Path:  "/one",
+			},
+		},
+	}
+
+	assert.NoError(t, db.Save(&album).Error)
+
+	user, err := models.RegisterUser(db, "user", nil, false)
+	assert.NoError(t, err)
+
+	db.Model(&user).Association("Albums").Append(album.ParentAlbum.ParentAlbum)
+
+	albumPath, err := actions.AlbumPath(db, user, &album)
+	assert.NoError(t, err)
+	assert.Len(t, albumPath, 2)
+	assert.Equal(t, "Two", albumPath[0].Title)
+	assert.Equal(t, "One", albumPath[1].Title)
+}
+
 func TestAlbumCover(t *testing.T) {
 	db := test_utils.DatabaseTest(t)
 
diff --git a/api/graphql/resolvers/album.go b/api/graphql/resolvers/album.go
index b89388e..d57df3b 100644
--- a/api/graphql/resolvers/album.go
+++ b/api/graphql/resolvers/album.go
@@ -131,38 +131,7 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.
 		return empty, nil
 	}
 
-	var album_path []*models.Album
-
-	err := r.Database.Raw(`
-		WITH recursive path_albums AS (
-			SELECT * FROM albums anchor WHERE anchor.id = ?
-			UNION
-			SELECT parent.* FROM path_albums child JOIN albums parent ON parent.id = child.parent_album_id
-		)
-		SELECT * FROM path_albums WHERE id != ?
-	`, obj.ID, obj.ID).Scan(&album_path).Error
-
-	// Make sure to only return albums this user owns
-	for i := len(album_path) - 1; i >= 0; i-- {
-		album := album_path[i]
-
-		owns, err := user.OwnsAlbum(r.Database, album)
-		if err != nil {
-			return nil, err
-		}
-
-		if !owns {
-			album_path = album_path[i+1:]
-			break
-		}
-
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	return album_path, nil
+	return actions.AlbumPath(r.Database, user, obj)
 }
 
 // Takes album_id, resets album.cover_id to 0 (null)

From bb19d18c42e69f92192bd68aadf0c7ce151c9bd6 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Mon, 27 Sep 2021 21:20:47 +0200
Subject: [PATCH 4/5] Add test for user favorite media

---
 api/graphql/models/user_test.go | 46 +++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/api/graphql/models/user_test.go b/api/graphql/models/user_test.go
index d0a1cf6..1daf38a 100644
--- a/api/graphql/models/user_test.go
+++ b/api/graphql/models/user_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/photoview/photoview/api/dataloader"
 	"github.com/photoview/photoview/api/graphql/models"
 	"github.com/photoview/photoview/api/test_utils"
 	"github.com/stretchr/testify/assert"
@@ -183,3 +184,48 @@ func TestUserOwnsAlbum(t *testing.T) {
 	assert.NoError(t, err)
 	assert.False(t, owns)
 }
+
+func TestUserFavoriteMedia(t *testing.T) {
+	db := test_utils.DatabaseTest(t)
+
+	user, err := models.RegisterUser(db, "user1", nil, false)
+	assert.NoError(t, err)
+
+	rootAlbum := models.Album{
+		Title: "root",
+		Path:  "/photos",
+	}
+
+	assert.NoError(t, db.Save(&rootAlbum).Error)
+	assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
+
+	media := models.Media{
+		Title:   "pic1",
+		Path:    "/photos/pic1",
+		AlbumID: rootAlbum.ID,
+	}
+
+	assert.NoError(t, db.Save(&media).Error)
+
+	// test that it starts out being false
+	favourite, err := dataloader.NewUserFavoriteLoader(db).Load(&models.UserMediaData{
+		UserID:  user.ID,
+		MediaID: media.ID,
+	})
+
+	assert.NoError(t, err)
+	assert.False(t, favourite)
+
+	favMedia, err := user.FavoriteMedia(db, media.ID, true)
+	assert.NoError(t, err)
+	assert.NotNil(t, favMedia)
+
+	// test that it is now true
+	favourite, err = dataloader.NewUserFavoriteLoader(db).Load(&models.UserMediaData{
+		UserID:  user.ID,
+		MediaID: media.ID,
+	})
+
+	assert.NoError(t, err)
+	assert.True(t, favourite)
+}

From c977f076dba7076244405b8b6dcfc4bd4b1ed10a Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Mon, 27 Sep 2021 21:26:30 +0200
Subject: [PATCH 5/5] Fix timeline for sqlite

---
 api/graphql/models/actions/photoview_test.db   | Bin 0 -> 167936 bytes
 api/graphql/models/actions/timeline_actions.go |  10 ++++++++--
 2 files changed, 8 insertions(+), 2 deletions(-)
 create mode 100644 api/graphql/models/actions/photoview_test.db

diff --git a/api/graphql/models/actions/photoview_test.db b/api/graphql/models/actions/photoview_test.db
new file mode 100644
index 0000000000000000000000000000000000000000..51babdaa122b7adb2551a11ebb666ddf57435d4c
GIT binary patch
diff --git a/api/graphql/models/actions/timeline_actions.go b/api/graphql/models/actions/timeline_actions.go
index 903b874..9ca7ca3 100644
--- a/api/graphql/models/actions/timeline_actions.go
+++ b/api/graphql/models/actions/timeline_actions.go
@@ -13,14 +13,20 @@ func MyTimeline(db *gorm.DB, user *models.User, paginate *models.Pagination, onl
 		Joins("JOIN albums ON media.album_id = albums.id").
 		Where("albums.id IN (?)", db.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID))
 
-	if db.Dialector.Name() == "postgres" {
+	switch db.Dialector.Name() {
+	case "postgres":
 		query = query.
 			Order("DATE_TRUNC('year', date_shot) DESC").
 			Order("DATE_TRUNC('month', date_shot) DESC").
 			Order("DATE_TRUNC('day', date_shot) DESC").
 			Order("albums.title ASC").
 			Order("media.date_shot DESC")
-	} else {
+	case "sqlite":
+		query = query.
+			Order("strftime('%j', media.date_shot) DESC"). // convert to day of year 001-366
+			Order("albums.title ASC").
+			Order("TIME(media.date_shot) DESC")
+	default:
 		query = query.
 			Order("YEAR(media.date_shot) DESC").
 			Order("MONTH(media.date_shot) DESC").
