diff --git a/go.mod b/go.mod
index 8b0e765..e01853b 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/mattn/go-sqlite3 v1.14.12 // indirect
 	github.com/mewkiz/flac v1.0.7 // indirect
 	github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect
 	github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
diff --git a/go.sum b/go.sum
index 5076b1c..537c85b 100644
--- a/go.sum
+++ b/go.sum
@@ -48,6 +48,8 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
+github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
 github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
 github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
diff --git a/main.go b/main.go
index 29d8c6d..92087a8 100644
--- a/main.go
+++ b/main.go
@@ -13,8 +13,9 @@ func main() {
 		log.Fatal("Expected a path to some music")
 	}
 
-	p := tea.NewProgram(gomus.NewModel(gomus.ModelArgs{
+	p := tea.NewProgram(gomus.NewModel(gomus.ModelConfig{
 		MusicPath: os.Args[1],
+		GomusPath: ".gomus",
 	}))
 
 	if err := p.Start(); err != nil {
diff --git a/pkg/commands.go b/pkg/commands.go
index 66bd251..4c17fbe 100644
--- a/pkg/commands.go
+++ b/pkg/commands.go
@@ -31,3 +31,13 @@ func newTrackVolumeCmd(volume float64) tea.Cmd {
 		return trackVolumeMsg{volume}
 	}
 }
+
+type libraryUpdateMsg struct {
+	tracks []track
+}
+
+func newLibraryUpdateCmd(tracks []track) tea.Cmd {
+	return func() tea.Msg {
+		return libraryUpdateMsg{tracks}
+	}
+}
diff --git a/pkg/list_delegate.go b/pkg/list_delegate.go
index d01dcef..db33961 100644
--- a/pkg/list_delegate.go
+++ b/pkg/list_delegate.go
@@ -24,7 +24,7 @@ func (d trackListDelegate) Render(w io.Writer, m list.Model, index int, listItem
 		return
 	}
 
-	fmt.Fprintf(w, f)
+	fmt.Fprintf(w, fmt.Sprintf("    %s", f))
 }
 
 func newTrackListDelegate() trackListDelegate {
diff --git a/pkg/model.go b/pkg/model.go
index f39e1c7..afc5aab 100644
--- a/pkg/model.go
+++ b/pkg/model.go
@@ -1,10 +1,11 @@
 package gomus
 
 import (
-	"time"
+	"errors"
+	"os"
 
 	tea "github.com/charmbracelet/bubbletea"
-	"github.com/faiface/beep/speaker"
+	"github.com/faiface/beep"
 )
 
 var (
@@ -12,8 +13,9 @@ var (
 	termHeight = 0
 )
 
-type ModelArgs struct {
+type ModelConfig struct {
 	MusicPath string
+	GomusPath string
 }
 
 type Model struct {
@@ -23,28 +25,32 @@ type Model struct {
 	TrackPlayer
 	TrackPlayerEffects
 	trackIndex
-
 	trackPlayerView
 }
 
-func NewModel(args ModelArgs) Model {
-	ti := NewDirTrackIndex(args.MusicPath)
-	tpv := newTrackPlayerView(ti.tracks)
+func NewModel(cfg ModelConfig) Model {
+	if _, err := os.Stat(cfg.GomusPath); errors.Is(err, os.ErrNotExist) {
+		err := os.Mkdir(cfg.GomusPath, 0755)
+		check(err)
+	}
 
 	return Model{
 		cursor:           0,
 		currentlyPlaying: 0,
 
-		trackIndex:         ti,
-		TrackPlayer:        TrackPlayer{},
-		TrackPlayerEffects: newTrackPlayerEffects(),
-
-		trackPlayerView: tpv,
+		trackIndex:         NewDirTrackIndex(cfg),
+		TrackPlayer:        NewTrackPlayer(),
+		TrackPlayerEffects: NewTrackPlayerEffects(),
+		trackPlayerView:    NewTrackPlayerView(),
 	}
 }
 
 func (m Model) Init() tea.Cmd {
-	return tea.Batch(tea.EnterAltScreen, m.trackPlayerView.Init())
+	var cmds []tea.Cmd
+	cmds = append(cmds, tea.EnterAltScreen)
+	cmds = append(cmds, m.trackPlayerView.Init())
+	cmds = append(cmds, newLibraryUpdateCmd(m.trackIndex.tracks))
+	return tea.Batch(cmds...)
 }
 
 func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -57,7 +63,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "ctrl+c", "q":
-			m.TrackPlayer.Close()
 			return m, tea.Quit
 		case "enter":
 			t := m.trackPlayerView.trackList.SelectedItem().(track)
@@ -65,35 +70,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			stream, format, err := t.GetStream()
 			check(err)
 
-			speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
+			resampled := beep.Resample(4, format.SampleRate, m.TrackPlayer.SampleRate, stream)
+			m.TrackPlayer.Play(resampled)
 
-			m.TrackPlayer.Play(&stream, &m.TrackPlayerEffects)
 			cmds = append(cmds, newTrackChangeCmd(t))
 		case " ":
-			if m.TrackPlayer.playerCtrl != nil {
-				s := m.TrackPlayer.TogglePause()
-				cmds = append(cmds, newTrackPauseCmd(s))
-			}
+			s := m.TrackPlayer.TogglePause()
+			cmds = append(cmds, newTrackPauseCmd(s))
 		case "-", "=":
-			v := &m.TrackPlayerEffects.volume
-
+			pe := &m.TrackPlayerEffects
 			if msg.String() == "-" {
-				*v -= 0.1
-				if *v < minVolume {
-					*v = minVolume
+				pe.volume -= 0.1
+				if pe.volume < minVolume {
+					pe.volume = minVolume
 				}
 			} else {
-				*v += 0.1
-				if *v > maxVolume {
-					*v = maxVolume
+				pe.volume += 0.1
+				if pe.volume > maxVolume {
+					pe.volume = maxVolume
 				}
 			}
 
-			if m.TrackPlayer.playerVol != nil {
-				m.TrackPlayer.SetVolume(*v)
-			}
+			m.TrackPlayer.SetVolume(pe.volume)
 
-			cmds = append(cmds, newTrackVolumeCmd(*v))
+			cmds = append(cmds, newTrackVolumeCmd(m.TrackPlayerEffects.volume))
 		}
 	}
 
diff --git a/pkg/track.go b/pkg/track.go
new file mode 100644
index 0000000..232d45c
--- /dev/null
+++ b/pkg/track.go
@@ -0,0 +1,74 @@
+package gomus
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/faiface/beep"
+	bflac "github.com/faiface/beep/flac"
+	"github.com/mewkiz/flac"
+	"github.com/mewkiz/flac/meta"
+)
+
+type track struct {
+	Title       string
+	Album       string
+	Artist      string
+	TrackPath   string
+	TrackTotal  uint8
+	TrackNumber uint8
+}
+
+func (t track) FilterValue() string { return "" }
+func (t track) fullName() string    { return fmt.Sprintf("%s - %s", t.Artist, t.Title) }
+
+func (t track) GetStream() (beep.StreamSeekCloser, beep.Format, error) {
+	f, err := os.Open(t.TrackPath)
+	check(err)
+
+	if strings.HasSuffix(t.TrackPath, ".flac") {
+		streamer, format, err := bflac.Decode(f)
+		check(err)
+		return streamer, format, nil
+	}
+
+	return nil, beep.Format{}, fmt.Errorf("Could not parse track")
+}
+
+func TrackFromFlac(path string) track {
+	path, err := filepath.Abs(path)
+	check(err)
+
+	s, err := flac.ParseFile(path)
+	check(err)
+
+	t := track{TrackPath: path}
+	for _, block := range s.Blocks {
+		if block.Header.Type == meta.TypeVorbisComment {
+			c := block.Body.(*meta.VorbisComment)
+			for _, tagTuple := range c.Tags {
+				tag, val := tagTuple[0], tagTuple[1]
+				switch tag {
+				case "TITLE":
+					t.Title = val
+				case "ARTIST":
+					t.Artist = val
+				case "ALBUM":
+					t.Album = val
+				case "TRACKNUMBER":
+					trackNum, err := strconv.ParseUint(val, 10, 8)
+					check(err)
+					t.TrackNumber = uint8(trackNum)
+				case "TRACKTOTAL":
+					trackTotal, err := strconv.ParseUint(val, 10, 8)
+					check(err)
+					t.TrackTotal = uint8(trackTotal)
+				}
+			}
+		}
+	}
+	return t
+}
diff --git a/pkg/track_fetcher.go b/pkg/track_fetcher.go
index 335c1d5..bb96a47 100644
--- a/pkg/track_fetcher.go
+++ b/pkg/track_fetcher.go
@@ -1,76 +1,80 @@
 package gomus
 
 import (
-	"fmt"
-	"io/ioutil"
-	"os"
+	"database/sql"
+	"io/fs"
+	"log"
 	"path/filepath"
 	"strings"
 
-	"github.com/faiface/beep"
-	bflac "github.com/faiface/beep/flac"
-	"github.com/mewkiz/flac"
-	"github.com/mewkiz/flac/meta"
+	_ "github.com/mattn/go-sqlite3"
 )
 
-type track struct {
-	Name      string
-	Artist    string
-	TrackPath string
-}
-
-func (t track) FilterValue() string { return "" }
-func (t track) fullName() string    { return fmt.Sprintf("%s - %s", t.Artist, t.Name) }
-
-func (t track) GetStream() (beep.StreamSeekCloser, beep.Format, error) {
-	f, err := os.Open(t.TrackPath)
-	check(err)
-
-	if strings.HasSuffix(t.TrackPath, ".flac") {
-		streamer, format, err := bflac.Decode(f)
-		check(err)
-		return streamer, format, nil
-	}
-
-	return nil, beep.Format{}, fmt.Errorf("Could not parse track")
-}
-
-func TrackFromFlac(path string) track {
-	s, err := flac.ParseFile(path)
-	check(err)
-
-	t := track{TrackPath: path}
-	for _, block := range s.Blocks {
-		if block.Header.Type == meta.TypeVorbisComment {
-			c := block.Body.(*meta.VorbisComment)
-			for _, tag := range c.Tags {
-				if tag[0] == "ARTIST" {
-					t.Artist = tag[1]
-				} else if tag[0] == "TITLE" {
-					t.Name = tag[1]
-				}
-			}
-		}
-	}
-
-	return t
-}
-
 type trackIndex struct {
 	tracks []track
+	db     *sql.DB
 }
 
-func NewDirTrackIndex(path string) trackIndex {
-	files, err := ioutil.ReadDir(path)
-	check(err)
-
-	tracks := []track{}
-	for _, file := range files {
-		if !file.IsDir() && strings.HasSuffix(file.Name(), ".flac") {
-			p := filepath.Join(path, file.Name())
-			tracks = append(tracks, TrackFromFlac(p))
-		}
+func NewDirTrackIndex(cfg ModelConfig) trackIndex {
+	dbPath := filepath.Join(cfg.GomusPath, "gomus.db")
+	db, err := sql.Open("sqlite3", dbPath)
+	if err != nil {
+		log.Fatalf("Failed to open gomus database: %v", err)
 	}
 
-	return trackIndex{tracks: tracks}
+	tblStmt := `
+	CREATE TABLE IF NOT EXISTS track (
+		title TEXT NOT NULL,
+		album TEXT,
+		artist TEXT NOT NULL,
+		uri TEXT NOT NULL,
+		trackNumber INTEGER,
+		trackTotal INTEGER
+	);
+	`
+
+	_, err = db.Exec(tblStmt)
+	if err != nil {
+		log.Fatalf("%q: %s\n", err, tblStmt)
+	}
+
+	path := cfg.MusicPath
+	tracks, err := ScanDirectory(path)
+	check(err)
+
+	ti := trackIndex{tracks, db}
+	ti.IndexTracks(tracks)
+
+	return ti
+}
+
+func (ti trackIndex) IndexTracks(tracks []track) error {
+	tx, err := ti.db.Begin()
+	check(err)
+
+	stmt, err := tx.Prepare("insert into track(title, album, artist, uri, trackNumber, trackTotal) values (?, ?, ?, ?, ?, ?)")
+	check(err)
+
+	for _, t := range tracks {
+		stmt.Exec(t.Title, t.Album, t.Artist, t.TrackPath, t.TrackNumber, t.TrackTotal)
+	}
+
+	err = tx.Commit()
+	check(err)
+
+	return nil
+}
+
+func ScanDirectory(path string) ([]track, error) {
+	tracks := []track{}
+	filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
+		check(err)
+		if !d.IsDir() && strings.HasSuffix(d.Name(), ".flac") {
+			t := TrackFromFlac(path)
+			tracks = append(tracks, t)
+		}
+		return nil
+	})
+
+	return tracks, nil
 }
diff --git a/pkg/track_player.go b/pkg/track_player.go
index 7168396..7e7538d 100644
--- a/pkg/track_player.go
+++ b/pkg/track_player.go
@@ -1,6 +1,8 @@
 package gomus
 
 import (
+	"time"
+
 	"github.com/faiface/beep"
 	"github.com/faiface/beep/effects"
 	"github.com/faiface/beep/speaker"
@@ -17,44 +19,51 @@ type TrackPlayerEffects struct {
 	volume float64
 }
 
-func newTrackPlayerEffects() TrackPlayerEffects {
+func NewTrackPlayerEffects() TrackPlayerEffects {
 	return TrackPlayerEffects{volume: startVolume}
 }
 
 type TrackPlayer struct {
-	streamer   *beep.StreamSeekCloser
-	playerCtrl *beep.Ctrl
-	playerVol  *effects.Volume
+	*beep.Ctrl
+	*effects.Volume
+
+	beep.SampleRate
 }
 
-func (t *TrackPlayer) Play(streamer *beep.StreamSeekCloser, trackEffects *TrackPlayerEffects) {
-	if t.streamer != nil {
-		t.Close()
+func NewTrackPlayer() TrackPlayer {
+	sr := beep.SampleRate(44100)
+	speaker.Init(sr, sr.N(time.Second/10))
+
+	ctrl := &beep.Ctrl{Streamer: beep.Silence(1), Paused: false}
+	volume := &effects.Volume{Streamer: ctrl, Base: base, Volume: startVolume, Silent: false}
+
+	return TrackPlayer{
+		SampleRate: sr,
+		Ctrl:       ctrl,
+		Volume:     volume,
 	}
+}
 
-	ctrl := &beep.Ctrl{Streamer: beep.Loop(-1, *streamer), Paused: false}
-	volume := &effects.Volume{Streamer: ctrl, Base: base, Volume: trackEffects.volume, Silent: false}
-	speaker.Play(volume)
+func (t *TrackPlayer) Play(streamer beep.Streamer) {
+	speaker.Clear()
 
-	t.streamer = streamer
-	t.playerCtrl = ctrl
-	t.playerVol = volume
+	speaker.Lock()
+	t.Ctrl.Streamer = streamer
+	speaker.Unlock()
+
+	speaker.Play(t.Volume)
 }
 
 func (t *TrackPlayer) TogglePause() bool {
 	speaker.Lock()
-	newState := !t.playerCtrl.Paused
-	t.playerCtrl.Paused = newState
+	newState := !t.Ctrl.Paused
+	t.Ctrl.Paused = newState
 	speaker.Unlock()
 	return newState
 }
 
 func (t *TrackPlayer) SetVolume(volume float64) {
 	speaker.Lock()
-	(*t.playerVol).Volume = volume
+	t.Volume.Volume = volume
 	speaker.Unlock()
 }
-
-func (t *TrackPlayer) Close() {
-	(*t.streamer).Close()
-}
diff --git a/pkg/track_player_view.go b/pkg/track_player_view.go
index 87e2cb7..6f55561 100644
--- a/pkg/track_player_view.go
+++ b/pkg/track_player_view.go
@@ -11,20 +11,15 @@ type trackPlayerView struct {
 	statusBar
 }
 
-func newTrackPlayerView(tracks []track) trackPlayerView {
-	c := mapList(tracks, func(t track) list.Item {
-		return t
-	})
-
-	l := list.New(c, newTrackListDelegate(), 0, 0)
+func NewTrackPlayerView() trackPlayerView {
+	l := list.New([]list.Item{}, newTrackListDelegate(), 0, 0)
 	l.SetShowTitle(false)
 	l.SetShowStatusBar(false)
 	l.SetShowHelp(false)
 	l.SetFilteringEnabled(false)
-
+	l.SetShowPagination(false)
 	s := statusBar{currentVolume: startVolume}
-
-	return trackPlayerView{trackList: l, statusBar: s}
+	return trackPlayerView{statusBar: s, trackList: l}
 }
 
 func (v trackPlayerView) Init() tea.Cmd {
@@ -42,6 +37,11 @@ func (v trackPlayerView) Update(msg tea.Msg) (trackPlayerView, tea.Cmd) {
 	case tea.WindowSizeMsg:
 		v.trackList.SetHeight(msg.Height - 2)
 		v.trackList.SetWidth(msg.Width)
+	case libraryUpdateMsg:
+		c := MapList(msg.tracks, func(t track) list.Item {
+			return t
+		})
+		v.trackList.SetItems(c)
 	}
 
 	var cmd tea.Cmd