diff --git a/db/migration/20210430212322_add_bpm_metadata.go b/db/migration/20210430212322_add_bpm_metadata.go
new file mode 100644
index 00000000..3fc0c865
--- /dev/null
+++ b/db/migration/20210430212322_add_bpm_metadata.go
@@ -0,0 +1,30 @@
+package migrations
+
+import (
+ "database/sql"
+
+ "github.com/pressly/goose"
+)
+
+func init() {
+ goose.AddMigration(upAddBpmMetadata, downAddBpmMetadata)
+}
+
+func upAddBpmMetadata(tx *sql.Tx) error {
+ _, err := tx.Exec(`
+alter table media_file
+ add bpm integer;
+
+create index if not exists media_file_bpm
+ on media_file (bpm);
+`)
+ if err != nil {
+ return err
+ }
+ notice(tx, "A full rescan needs to be performed to import more tags")
+ return forceFullRescan(tx)
+}
+
+func downAddBpmMetadata(tx *sql.Tx) error {
+ return nil
+}
diff --git a/model/mediafile.go b/model/mediafile.go
index 859fe226..f65f0916 100644
--- a/model/mediafile.go
+++ b/model/mediafile.go
@@ -39,6 +39,7 @@ type MediaFile struct {
Compilation bool `json:"compilation"`
Comment string `json:"comment"`
Lyrics string `json:"lyrics"`
+ Bpm int `json:"bpm,omitempty"`
CatalogNum string `json:"catalogNum"`
MbzTrackID string `json:"mbzTrackId" orm:"column(mbz_track_id)"`
MbzAlbumID string `json:"mbzAlbumId" orm:"column(mbz_album_id)"`
diff --git a/scanner/mapping.go b/scanner/mapping.go
index 6ee2bf23..7f99e537 100644
--- a/scanner/mapping.go
+++ b/scanner/mapping.go
@@ -64,7 +64,7 @@ func (s *mediaFileMapper) toMediaFile(md metadata.Metadata) model.MediaFile {
mf.MbzAlbumComment = md.MbzAlbumComment()
mf.Comment = s.policy.Sanitize(md.Comment())
mf.Lyrics = s.policy.Sanitize(md.Lyrics())
-
+ mf.Bpm = md.Bpm()
mf.CreatedAt = time.Now()
mf.UpdatedAt = md.ModificationTime()
diff --git a/scanner/metadata/ffmpeg_test.go b/scanner/metadata/ffmpeg_test.go
index 0d6f0ff4..33c0db9a 100644
--- a/scanner/metadata/ffmpeg_test.go
+++ b/scanner/metadata/ffmpeg_test.go
@@ -286,4 +286,21 @@ Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hu
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/one.mp3", "-i", "/music library/two.mp3", "-f", "ffmetadata"}))
})
+ It("parses an integer TBPM tag", func() {
+ const output = `
+ Input #0, mp3, from 'tests/fixtures/test.mp3':
+ Metadata:
+ TBPM : 123`
+ md, _ := e.extractMetadata("tests/fixtures/test.mp3", output)
+ Expect(md.Bpm()).To(Equal(123))
+ })
+
+ It("parses and rounds a floating point fBPM tag", func() {
+ const output = `
+ Input #0, ogg, from 'tests/fixtures/test.ogg':
+ Metadata:
+ FBPM : 141.7`
+ md, _ := e.extractMetadata("tests/fixtures/test.ogg", output)
+ Expect(md.Bpm()).To(Equal(142))
+ })
})
diff --git a/scanner/metadata/metadata.go b/scanner/metadata/metadata.go
index 137482ad..a4ca658c 100644
--- a/scanner/metadata/metadata.go
+++ b/scanner/metadata/metadata.go
@@ -2,6 +2,7 @@ package metadata
import (
"fmt"
+ "math"
"os"
"path"
"regexp"
@@ -66,6 +67,7 @@ type Metadata interface {
FilePath() string
Suffix() string
Size() int64
+ Bpm() int
}
type baseMetadata struct {
@@ -127,6 +129,15 @@ func (m *baseMetadata) Suffix() string {
func (m *baseMetadata) Duration() float32 { panic("not implemented") }
func (m *baseMetadata) BitRate() int { panic("not implemented") }
func (m *baseMetadata) HasPicture() bool { panic("not implemented") }
+func (m *baseMetadata) Bpm() int {
+ var bpmStr = m.getTag("tbpm", "bpm", "fbpm")
+ var bpmFloat, err = strconv.ParseFloat(bpmStr, 64)
+ if err == nil {
+ return (int)(math.Round(bpmFloat))
+ } else {
+ return 0
+ }
+}
func (m *baseMetadata) parseInt(tagName string) int {
if v, ok := m.tags[tagName]; ok {
diff --git a/scanner/metadata/taglib_test.go b/scanner/metadata/taglib_test.go
index 82c44ad5..8c638827 100644
--- a/scanner/metadata/taglib_test.go
+++ b/scanner/metadata/taglib_test.go
@@ -33,19 +33,20 @@ var _ = Describe("taglibExtractor", func() {
Expect(m.BitRate()).To(Equal(192))
Expect(m.FilePath()).To(Equal("tests/fixtures/test.mp3"))
Expect(m.Suffix()).To(Equal("mp3"))
- Expect(m.Size()).To(Equal(int64(60845)))
+ Expect(m.Size()).To(Equal(int64(51876)))
Expect(m.Comment()).To(Equal("Comment1\nComment2"))
+ Expect(m.Bpm()).To(Equal(123))
- //TODO This file has some weird tags that makes the following tests fail sometimes.
- //m = mds["tests/fixtures/test.ogg"]
- //Expect(err).To(BeNil())
- //Expect(m.Title()).To(BeEmpty())
- //Expect(m.HasPicture()).To(BeFalse())
- //Expect(m.Duration()).To(Equal(float32(3)))
- //Expect(m.BitRate()).To(Equal(10))
- //Expect(m.Suffix()).To(Equal("ogg"))
- //Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
- //Expect(m.Size()).To(Equal(int64(4408)))
+ m = mds["tests/fixtures/test.ogg"]
+ Expect(err).To(BeNil())
+ Expect(m.Title()).To(BeEmpty())
+ Expect(m.HasPicture()).To(BeFalse())
+ Expect(m.Duration()).To(Equal(float32(1)))
+ Expect(m.BitRate()).To(Equal(39))
+ Expect(m.Suffix()).To(Equal("ogg"))
+ Expect(m.FilePath()).To(Equal("tests/fixtures/test.ogg"))
+ Expect(m.Size()).To(Equal(int64(5065)))
+ Expect(m.Bpm()).To(Equal(142)) // This file has a floating point BPM set to 141.7 under the fBPM tag. Ensure we parse and round correctly.
})
})
})
diff --git a/tests/fixtures/test.mp3 b/tests/fixtures/test.mp3
index e6941d36..6f7c494c 100644
Binary files a/tests/fixtures/test.mp3 and b/tests/fixtures/test.mp3 differ
diff --git a/tests/fixtures/test.ogg b/tests/fixtures/test.ogg
index 220f76f0..be672812 100644
Binary files a/tests/fixtures/test.ogg and b/tests/fixtures/test.ogg differ
diff --git a/ui/src/album/AlbumSongs.js b/ui/src/album/AlbumSongs.js
index b5bf6be3..9c521047 100644
--- a/ui/src/album/AlbumSongs.js
+++ b/ui/src/album/AlbumSongs.js
@@ -3,6 +3,7 @@ import {
BulkActionsToolbar,
ListToolbar,
TextField,
+ NumberField,
useVersion,
useListContext,
} from 'react-admin'
@@ -128,6 +129,7 @@ const AlbumSongs = (props) => {
{isDesktop && }
{isDesktop && }
+ {isDesktop && }
{isDesktop && config.enableStarRating && (
{
size: ,
updatedAt: ,
playCount: ,
+ bpm: ,
comment: ,
}
if (!record.discSubtitle) {
@@ -40,6 +47,9 @@ export const SongDetails = (props) => {
if (!record.comment) {
delete data.comment
}
+ if (!record.bpm) {
+ delete data.bpm
+ }
if (record.playCount > 0) {
data.playDate =
}
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index a957c047..c6904499 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -22,7 +22,8 @@
"starred": "Favourite",
"rating": "Rating",
"comment": "Comment",
- "quality": "Quality"
+ "quality": "Quality",
+ "bpm": "BPM"
},
"actions": {
"addToQueue": "Play Later",
diff --git a/ui/src/playlist/PlaylistSongs.js b/ui/src/playlist/PlaylistSongs.js
index 4d202d51..fbf6c901 100644
--- a/ui/src/playlist/PlaylistSongs.js
+++ b/ui/src/playlist/PlaylistSongs.js
@@ -3,6 +3,7 @@ import {
BulkActionsToolbar,
ListToolbar,
TextField,
+ NumberField,
useRefresh,
useDataProvider,
useNotify,
@@ -166,6 +167,7 @@ const PlaylistSongs = ({ playlistId, readOnly, actions, ...props }) => {
{isDesktop && }
{isDesktop && }
+ {isDesktop && }
{
)}
{isDesktop && }
+ {isDesktop && }
{config.enableStarRating && (