diff --git a/emmental/sidebar/artist.py b/emmental/sidebar/artist.py
new file mode 100644
index 0000000..1c7d286
--- /dev/null
+++ b/emmental/sidebar/artist.py
@@ -0,0 +1,56 @@
+# Copyright 2022 (c) Anna Schumaker.
+"""Displays our artist and album tree."""
+from gi.repository import GObject
+from gi.repository import Gtk
+from ..buttons import ImageToggle
+from .. import db
+from . import row
+from . import section
+
+
+class ArtistRow(row.TreeRow):
+ """A factory for setting Album covers properly."""
+
+ def __init__(self, *args, **kwargs):
+ """Initialize an ArtistRow."""
+ super().__init__(*args, **kwargs)
+ self.child = row.Row()
+
+ def do_bind(self) -> None:
+ """Bind the album cover property."""
+ super().do_bind()
+ if isinstance(self.item, db.albums.Album):
+ self.bind_and_set_property("cover", "image")
+
+
+class Section(section.Section):
+ """A sidebar Section for the artist and album playlist tree."""
+
+ album_table = GObject.Property(type=db.albums.Table)
+ show_all = GObject.Property(type=bool, default=False)
+
+ def __init__(self, artist_table: db.artists.Table,
+ album_table: db.albums.Table):
+ """Initialize our artist & album section."""
+ super().__init__(artist_table, ArtistRow, title="Artists & Albums",
+ subtitle="0 artists, 0 albums",
+ icon_name="library-artists", album_table=album_table)
+ self.extra_widget = ImageToggle("music-artist", "music-artist2",
+ icon_size=Gtk.IconSize.NORMAL,
+ has_frame=False)
+ self.album_table.connect("items-changed", self.__update_subtitle)
+ self.bind_property("show-all", self.extra_widget, "active",
+ GObject.BindingFlags.BIDIRECTIONAL)
+ self.bind_property("show-all", artist_table, "show-all")
+
+ def __update_subtitle(self, table: db.albums.Table, position: int,
+ removed: int, added: int) -> None:
+ self.subtitle = self.do_get_subtitle(0)
+
+ def do_get_subtitle(self, n_items: int) -> str:
+ """Return a subtitle for this section."""
+ n_artists = len(self.table)
+ s_artists = "s" if n_artists != 1 else ""
+ n_albums = len(self.album_table)
+ s_albums = "s" if n_albums != 1 else ""
+ return f"{n_artists} artist{s_artists}, {n_albums} album{s_albums}"
diff --git a/icons/scalable/actions/library-artists-symbolic.svg b/icons/scalable/actions/library-artists-symbolic.svg
new file mode 100644
index 0000000..ec6ecf9
--- /dev/null
+++ b/icons/scalable/actions/library-artists-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/icons/scalable/actions/music-artist-symbolic.svg b/icons/scalable/actions/music-artist-symbolic.svg
new file mode 100644
index 0000000..228e9a8
--- /dev/null
+++ b/icons/scalable/actions/music-artist-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/icons/scalable/actions/music-artist2-symbolic.svg b/icons/scalable/actions/music-artist2-symbolic.svg
new file mode 100644
index 0000000..e834152
--- /dev/null
+++ b/icons/scalable/actions/music-artist2-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/tests/sidebar/test_artist.py b/tests/sidebar/test_artist.py
new file mode 100644
index 0000000..1dd76ad
--- /dev/null
+++ b/tests/sidebar/test_artist.py
@@ -0,0 +1,89 @@
+# Copyright 2022 (c) Anna Schumaker.
+"""Tests our artist / album / disc section and tree."""
+import emmental.sidebar.artist
+import emmental.sidebar.row
+import tests.util
+import unittest.mock
+from gi.repository import Gtk
+
+
+class TestArtist(tests.util.TestCase):
+ """Test our Artist section."""
+
+ def setUp(self):
+ """Set up common variables."""
+ super().setUp()
+ self.artists = emmental.sidebar.artist.Section(self.sql.artists,
+ self.sql.albums)
+
+ def test_init(self):
+ """Test that the artists section is set up correctly."""
+ self.assertIsInstance(self.artists, emmental.sidebar.section.Section)
+ self.assertEqual(self.artists._factory.row_type,
+ emmental.sidebar.artist.ArtistRow)
+
+ self.assertEqual(self.artists.table, self.sql.artists)
+ self.assertEqual(self.artists.album_table, self.sql.albums)
+ self.assertEqual(self.artists.icon_name, "library-artists")
+ self.assertEqual(self.artists.title, "Artists & Albums")
+
+ def test_extra_widget(self):
+ """Test the artist section extra widget."""
+ self.assertIsInstance(self.artists.extra_widget,
+ emmental.buttons.ImageToggle)
+ self.assertEqual(self.artists.extra_widget.active_icon_name,
+ "music-artist")
+ self.assertEqual(self.artists.extra_widget.inactive_icon_name,
+ "music-artist2")
+ self.assertEqual(self.artists.extra_widget.icon_size,
+ Gtk.IconSize.NORMAL)
+ self.assertFalse(self.artists.extra_widget.get_has_frame())
+
+ def test_subtitle(self):
+ """Test that the subtitle property is set properly."""
+ self.artists.show_all = True
+ self.assertEqual(self.artists.subtitle, "0 artists, 0 albums")
+ artist = self.sql.artists.create("Artist 1")
+ self.assertEqual(self.artists.subtitle, "1 artist, 0 albums")
+ artist.add_album(self.sql.albums.create("Album 1", "Artist 1", "2022"))
+ self.assertEqual(self.artists.subtitle, "1 artist, 1 album")
+ artist.add_album(self.sql.albums.create("Album 2", "Artist 1", "2023"))
+ self.assertEqual(self.artists.subtitle, "1 artist, 2 albums")
+ self.sql.artists.create("Artist 2")
+ self.assertEqual(self.artists.subtitle, "2 artists, 2 albums")
+
+ def test_show_all(self):
+ """Test the show all property."""
+ self.assertFalse(self.artists.show_all)
+ self.artists.show_all = True
+ self.assertTrue(self.artists.extra_widget.active)
+ self.assertTrue(self.sql.artists.show_all)
+ self.artists.extra_widget.active = False
+ self.assertFalse(self.artists.show_all)
+ self.assertFalse(self.sql.artists.show_all)
+
+ def test_artist_row(self):
+ """Test setting an album cover to the row icon."""
+ artist = self.sql.artists.create("Test Artist")
+ album = self.sql.albums.create("Test Album", "Test Artist", "2022")
+ artist.add_album(album)
+
+ treeitem = Gtk.TreeListRow()
+ treeitem.get_item = unittest.mock.Mock(return_value=artist)
+ listitem = Gtk.ListItem()
+ listitem.get_item = unittest.mock.Mock(return_value=treeitem)
+ row = emmental.sidebar.artist.ArtistRow(listitem)
+ self.assertIsInstance(row.child, emmental.sidebar.row.Row)
+ self.assertTrue(row.indented)
+
+ row.bind()
+ self.assertEqual(row.child.name, "Test Artist")
+ row.unbind()
+
+ treeitem.get_item.return_value = album
+ row.bind()
+ self.assertEqual(row.child.name, "Test Album")
+ self.assertIsNone(row.child.image)
+
+ album.cover = tests.util.COVER_JPG
+ self.assertEqual(row.child.image, tests.util.COVER_JPG)