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 && (