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