diff --git a/conf/configuration.go b/conf/configuration.go
index 7a52b7c83..703467a9a 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -33,6 +33,8 @@ type nd struct {
 	ImageCacheSize          string `default:"100MB"` // in MB
 	ProbeCommand            string `default:"ffmpeg %s -f ffmetadata"`
 
+	CoverArtPriority string `default:"embedded, cover.*, folder.*, front.*"`
+
 	// DevFlags. These are used to enable/disable debugging and incomplete features
 	DevLogSourceLine           bool   `default:"false"`
 	DevAutoCreateAdminPassword string `default:""`
diff --git a/engine/cover.go b/engine/cover.go
index 00defa32a..4be93e106 100644
--- a/engine/cover.go
+++ b/engine/cover.go
@@ -11,6 +11,7 @@ import (
 	_ "image/png"
 	"io"
 	"os"
+	"path/filepath"
 	"strings"
 	"time"
 
@@ -91,6 +92,7 @@ func (c *cover) getCoverPath(ctx context.Context, id string) (path string, lastU
 	if found, err = c.ds.Album(ctx).Exists(id); err != nil {
 		return
 	}
+	var coverPath string
 	if found {
 		var al *model.Album
 		al, err = c.ds.Album(ctx).Get(id)
@@ -102,15 +104,22 @@ func (c *cover) getCoverPath(ctx context.Context, id string) (path string, lastU
 			return
 		}
 		id = al.CoverArtId
+		coverPath = al.CoverArtPath
 	}
 	var mf *model.MediaFile
 	mf, err = c.ds.MediaFile(ctx).Get(id)
-	if err != nil {
+	if err == nil && mf.HasCoverArt {
+		return mf.Path, mf.UpdatedAt, nil
+	} else if err != nil && coverPath != "" {
+		info, err := os.Stat(coverPath)
+		if err != nil {
+			return "", time.Time{}, model.ErrNotFound
+		}
+		return coverPath, info.ModTime(), nil
+	} else if err != nil {
 		return
 	}
-	if mf.HasCoverArt {
-		return mf.Path, mf.UpdatedAt, nil
-	}
+
 	return "", time.Time{}, model.ErrNotFound
 }
 
@@ -126,7 +135,17 @@ func (c *cover) getCover(ctx context.Context, path string, size int) (reader io.
 		}
 	}()
 	var data []byte
-	data, err = readFromTag(path)
+	for _, p := range strings.Split(conf.Server.CoverArtPriority, ",") {
+		pat := strings.ToLower(strings.TrimSpace(p))
+		if pat == "embedded" {
+			data, err = readFromTag(path)
+		} else if ok, _ := filepath.Match(pat, strings.ToLower(filepath.Base(path))); ok {
+			data, err = readFromFile(path)
+		}
+		if err == nil {
+			break
+		}
+	}
 
 	if err == nil && size > 0 {
 		data, err = resizeImage(bytes.NewReader(data), size)
@@ -171,6 +190,21 @@ func readFromTag(path string) ([]byte, error) {
 	return picture.Data, nil
 }
 
+func readFromFile(path string) ([]byte, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+
+	defer f.Close()
+	var buf bytes.Buffer
+	if _, err := buf.ReadFrom(f); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
 func NewImageCache() (ImageCache, error) {
 	return newFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems)
 }
diff --git a/persistence/album_repository.go b/persistence/album_repository.go
index 205f2f538..2776e5e7c 100644
--- a/persistence/album_repository.go
+++ b/persistence/album_repository.go
@@ -2,6 +2,8 @@ package persistence
 
 import (
 	"context"
+	"os"
+	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
@@ -9,6 +11,7 @@ import (
 
 	. "github.com/Masterminds/squirrel"
 	"github.com/astaxie/beego/orm"
+	"github.com/deluan/navidrome/conf"
 	"github.com/deluan/navidrome/consts"
 	"github.com/deluan/navidrome/log"
 	"github.com/deluan/navidrome/model"
@@ -137,9 +140,15 @@ func (r *albumRepository) Refresh(ids ...string) error {
 	toInsert := 0
 	toUpdate := 0
 	for _, al := range albums {
-		if !al.HasCoverArt {
-			al.CoverArtId = ""
+		if !al.HasCoverArt || !strings.HasPrefix(conf.Server.CoverArtPriority, "embedded") {
+			if path := getCoverFromPath(al.CoverArtPath, al.HasCoverArt); path != "" {
+				al.CoverArtId = "al-" + al.ID
+				al.CoverArtPath = path
+			} else if !al.HasCoverArt {
+				al.CoverArtId = ""
+			}
 		}
+
 		if al.Compilation {
 			al.AlbumArtist = consts.VariousArtists
 			al.AlbumArtistID = consts.VariousArtistsID
@@ -184,6 +193,42 @@ func getMinYear(years string) int {
 	return 0
 }
 
+// GetCoverFromPath accepts a path to a file, and returns a path to an eligible cover image from the
+// file's directory (as configured with CoverArtPriority). If no cover file is found, among
+// available choices, or an error occurs, an empty string is returned. If HasEmbeddedCover is true,
+// and 'embedded' is matched among eligible choices, GetCoverFromPath will return early with an
+// empty path.
+func getCoverFromPath(path string, hasEmbeddedCover bool) string {
+	n, err := os.Open(filepath.Dir(path))
+	if err != nil {
+		return ""
+	}
+
+	defer n.Close()
+	names, err := n.Readdirnames(-1)
+	if err != nil {
+		return ""
+	}
+
+	for _, p := range strings.Split(conf.Server.CoverArtPriority, ",") {
+		pat := strings.ToLower(strings.TrimSpace(p))
+		if pat == "embedded" {
+			if hasEmbeddedCover {
+				return ""
+			}
+			continue
+		}
+
+		for _, name := range names {
+			if ok, _ := filepath.Match(pat, strings.ToLower(name)); ok {
+				return filepath.Join(filepath.Dir(path), name)
+			}
+		}
+	}
+
+	return ""
+}
+
 func (r *albumRepository) purgeEmpty() error {
 	del := Delete(r.tableName).Where("id not in (select distinct(album_id) from media_file)")
 	c, err := r.executeSQL(del)