diff --git a/db/migration/20210430212322_add_bpm_metadata.go b/db/migration/20210430212322_add_bpm_metadata.go
new file mode 100644
index 000000000..3fc0c865f
--- /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 859fe2264..f65f09165 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 6ee2bf23e..7f99e5377 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 0d6f0ff42..33c0db9aa 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 137482adc..a4ca658c8 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 82c44ad57..8c638827f 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 e6941d360..6f7c494c9 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 220f76f0c..be6728129 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 b5bf6be32..9c5210478 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 && <TextField source="artist" sortable={false} />}
             <DurationField source="duration" sortable={false} />
             {isDesktop && <QualityInfo source="quality" sortable={false} />}
+            {isDesktop && <NumberField source="bpm" sortable={false} />}
             {isDesktop && config.enableStarRating && (
               <RatingField
                 source="rating"
diff --git a/ui/src/common/SongDetails.js b/ui/src/common/SongDetails.js
index 17676eb61..e95cf0264 100644
--- a/ui/src/common/SongDetails.js
+++ b/ui/src/common/SongDetails.js
@@ -5,7 +5,13 @@ import TableBody from '@material-ui/core/TableBody'
 import TableCell from '@material-ui/core/TableCell'
 import TableContainer from '@material-ui/core/TableContainer'
 import TableRow from '@material-ui/core/TableRow'
-import { BooleanField, DateField, TextField, useTranslate } from 'react-admin'
+import {
+  BooleanField,
+  DateField,
+  TextField,
+  NumberField,
+  useTranslate,
+} from 'react-admin'
 import inflection from 'inflection'
 import { BitrateField, SizeField } from './index'
 import { MultiLineTextField } from './MultiLineTextField'
@@ -32,6 +38,7 @@ export const SongDetails = (props) => {
     size: <SizeField record={record} source="size" />,
     updatedAt: <DateField record={record} source="updatedAt" showTime />,
     playCount: <TextField record={record} source="playCount" />,
+    bpm: <NumberField record={record} source="bpm" />,
     comment: <MultiLineTextField record={record} source="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 = <DateField record={record} source="playDate" showTime />
   }
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index a957c0473..c6904499b 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 4d202d51e..fbf6c901a 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 && <TextField source="artist" />}
               <DurationField source="duration" className={classes.draggable} />
               {isDesktop && <QualityInfo source="quality" sortable={false} />}
+              {isDesktop && <NumberField source="bpm" />}
               <SongContextMenu
                 onAddToPlaylist={onAddToPlaylist}
                 showLove={false}
diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js
index 03cada6d7..9aabf325d 100644
--- a/ui/src/song/SongList.js
+++ b/ui/src/song/SongList.js
@@ -121,6 +121,7 @@ const SongList = (props) => {
             )}
             {isDesktop && <QualityInfo source="quality" sortable={false} />}
             <DurationField source="duration" />
+            {isDesktop && <NumberField source="bpm" />}
             {config.enableStarRating && (
               <RatingField
                 source="rating"