Repository: aome510/spotify-player (language: rust)

Issue:
Podcast Support
**Is your feature already implemented in the latest `master`?**
No

**Is your feature request related to a problem? Please describe.**
No

**Describe the solution you'd like**
I would like to have podcast support available in the app. When I search for podcasts, I don't get any of the results I'm expecting. Possibly another pane with podcast results only. I haven't dabbled with Spotifys Podcast API so I'm not sure how the implementation would be done.



Discussion / hints:
It seems to kinda already support it? If I'm playing a podcast on my phone, then open spotify-player on the desktop and just hit "space" without actually choosing a song, the podcast I was listing to will start playing but nothing will show on playback. So I guess it technically already has that functionality, just no search.
I've seen that too! I was referring to functionality where you could search for, browse and play podcasts. But playback if started from another device seems supported!
Related: 
- https://github.com/aome510/spotify-player/issues/423
- https://github.com/aome510/spotify-player/issues/359

Podcast is not my priority since I never use it personally. PRs are highly welcome though!
Do you have any tips for how I would implement it? Do you think it would be a lot of work? @aome510 
> Do you have any tips

@fwastring From a UX perspective, I feel like `podcasts` and `podcast_episodes` should be top-level entities. For instance if a user searches for `foo`:

They should see results from these types:

```diff
tracks
albums
artists
playlists
+podcasts
+podcast_episodes
```

## Actions (context menu)

A `podcast` can have similar actions\* to an `artist`. Some modifications would be required:

```diff
-GoToArtistRadio
-CopyArtistLink
+CopyPodcastLink
Follow
```

A `podcast_episode` can have similar actions\* to a `track`. Some modifications would be required:

```diff
-GoToArtist
-GoToAlbum
-GoToTrackRadio
+GoToPodcast
CopyTrackLink
AddToPlaylist
AddToQueue
-AddToLikedTracks
```

\* By actions, I'm referring to the menu you can invoke by pressing `g a` when hovering over a list item (e.g. in the search results).

## Pages

The `podcast` detail page can be simple. It can contain a single list of `podcast_episodes` (similar in layout to the `album` detail page).

I don't see the need for `podcast_episode` detail page. Similar to how a `track` does not currently have it's own page.
Thanks @thenbe for the suggestions!

> Do you have any tips for how I would implement it? Do you think it would be a lot of work? @aome510

Sorry I missed the message. It should be relatively straightforward to add support for podcast as you can follow the already established patterns in the codebase. This is not a "good first issue" in a way that the implementation will need to touch many places in the codebase, which might require a certain level of familiarity.

Fix patch:
diff --git a/README.md b/README.md
index 856bb410..23b22d74 100644
--- a/README.md
+++ b/README.md
@@ -391,6 +391,7 @@ List of available actions:
 - `DeleteFromPlaylist`
 - `ShowActionsOnAlbum`
 - `ShowActionsOnArtist`
+- `ShowActionsOnShow`
 - `ToggleLiked`
 - `CopyLink`
 - `Follow`
diff --git a/lyric_finder/src/lib.rs b/lyric_finder/src/lib.rs
index 87048ddf..1749fb96 100644
--- a/lyric_finder/src/lib.rs
+++ b/lyric_finder/src/lib.rs
@@ -73,9 +73,10 @@ impl Client {
             .await?;
 
         if body.meta.status != 200 {
-            let message = match body.meta.message {
-                Some(m) => m,
-                None => format!("request failed with status code: {}", body.meta.status),
+            let message = if let Some(m) = body.meta.message {
+                m
+            } else {
+                format!("request failed with status code: {}", body.meta.status)
             };
             anyhow::bail!(message);
         }
@@ -226,14 +227,14 @@ mod parse {
             _ => false,
         };
 
-        Ok(parse_dom_node(&dom.document, &Some(filter), false))
+        Ok(parse_dom_node(&dom.document, Some(&filter), false))
     }
 
     /// Parse a dom node and extract the text of children nodes satisfying a requirement.
     ///
     /// The requirement is represented by a `filter` function and a `should_parse` variable.
     /// Once a node satisfies a requirement, its children should also satisfy it.
-    fn parse_dom_node<F>(node: &Handle, filter: &Option<F>, mut should_parse: bool) -> String
+    fn parse_dom_node<F>(node: &Handle, filter: Option<&F>, mut should_parse: bool) -> String
     where
         F: Fn(&NodeData) -> bool,
     {
diff --git a/spotify_player/src/cli/client.rs b/spotify_player/src/cli/client.rs
index aa96df9f..d1bad134 100644
--- a/spotify_player/src/cli/client.rs
+++ b/spotify_player/src/cli/client.rs
@@ -54,14 +54,15 @@ pub async fn start_socket(client: Client, socket: UdpSocket, state: Option<Share
                 let span = tracing::info_span!("socket_request", request = ?request, dest_addr = ?dest_addr);
 
                 async {
-                    let response = match handle_socket_request(&client, &state, request).await {
-                        Err(err) => {
-                            tracing::error!("Failed to handle socket request: {err:#}");
-                            let msg = format!("Bad request: {err:#}");
-                            Response::Err(msg.into_bytes())
-                        }
-                        Ok(data) => Response::Ok(data),
-                    };
+                    let response =
+                        match handle_socket_request(&client, state.as_ref(), request).await {
+                            Err(err) => {
+                                tracing::error!("Failed to handle socket request: {err:#}");
+                                let msg = format!("Bad request: {err:#}");
+                                Response::Err(msg.into_bytes())
+                            }
+                            Ok(data) => Response::Ok(data),
+                        };
                     send_response(response, &socket, dest_addr)
                         .await
                         .unwrap_or_default();
@@ -94,13 +95,13 @@ async fn send_response(
 
 async fn current_playback(
     client: &Client,
-    state: &Option<SharedState>,
+    state: Option<&SharedState>,
 ) -> Result<Option<CurrentPlaybackContext>> {
     // get current playback from the application's state, if exists, or by making an API request
     match state {
-        Some(ref state) => Ok(state.player.read().current_playback()),
+        Some(state) => Ok(state.player.read().current_playback()),
         None => client
-            .current_playback(None, None::<Vec<_>>)
+            .current_playback2()
             .await
             .context("get current playback"),
     }
@@ -108,7 +109,7 @@ async fn current_playback(
 
 async fn handle_socket_request(
     client: &Client,
-    state: &Option<SharedState>,
+    state: Option<&SharedState>,
     request: super::Request,
 ) -> Result<Vec<u8>> {
     if let Some(state) = state {
@@ -180,7 +181,7 @@ async fn handle_socket_request(
 
 async fn handle_get_key_request(
     client: &Client,
-    state: &Option<SharedState>,
+    state: Option<&SharedState>,
     key: Key,
 ) -> Result<Vec<u8>> {
     Ok(match key {
@@ -326,13 +327,13 @@ async fn handle_search_request(client: &Client, query: String) -> Result<Vec<u8>
 
 async fn handle_playback_request(
     client: &Client,
-    state: &Option<SharedState>,
+    state: Option<&SharedState>,
     command: Command,
 ) -> Result<()> {
     let playback = if let Some(state) = state {
         state.player.read().buffered_playback.clone()
     } else {
-        let playback = client.current_playback(None, None::<Vec<_>>).await?;
+        let playback = client.current_playback2().await?;
         playback.as_ref().map(PlaybackMetadata::from_playback)
     };
 
@@ -342,27 +343,27 @@ async fn handle_playback_request(
             let tracks = client.radio_tracks(sid.uri()).await?;
 
             PlayerRequest::StartPlayback(
-                Playback::URIs(tracks.into_iter().map(|t| t.id).collect(), None),
+                Playback::URIs(tracks.into_iter().map(|t| t.id.into()).collect(), None),
                 None,
             )
         }
         Command::StartLikedTracks { limit, random } => {
             // get a list of liked tracks' ids
-            let mut ids: Vec<_> = if let Some(ref state) = state {
+            let mut ids: Vec<PlayableId> = if let Some(state) = state {
                 state
                     .data
                     .read()
                     .user_data
                     .saved_tracks
                     .values()
-                    .map(|t| t.id.clone())
+                    .map(|t| t.id.clone().into())
                     .collect()
             } else {
                 client
                     .current_user_saved_tracks()
                     .await?
                     .into_iter()
-                    .map(|t| t.id)
+                    .map(|t| t.id.into())
                     .collect()
             };
 
@@ -415,7 +416,7 @@ async fn handle_playback_request(
             // the function scope is from the application's state (cached) or the `current_playback` API.
             // Therefore, we need to make an additional API request to get the playback's progress.
             let progress = client
-                .current_playback(None, None::<Vec<_>>)
+                .current_playback2()
                 .await?
                 .context("no active playback found!")?
                 .progress
@@ -426,7 +427,7 @@ async fn handle_playback_request(
         }
     };
 
-    if let Some(ref state) = state {
+    if let Some(state) = state {
         // A non-null application's state indicates there is a running application instance.
         // To reduce the latency of the CLI command, the player request is handled asynchronously
         // knowing that the application will outlive the asynchronous task.
diff --git a/spotify_player/src/client/handlers.rs b/spotify_player/src/client/handlers.rs
index f253f2ad..b27ddd25 100644
--- a/spotify_player/src/client/handlers.rs
+++ b/spotify_player/src/client/handlers.rs
@@ -4,7 +4,7 @@ use tracing::Instrument;
 
 use crate::{
     config,
-    state::{ContextId, ContextPageType, ContextPageUIState, PageState, SharedState},
+    state::{ContextId, ContextPageType, ContextPageUIState, PageState, PlayableId, SharedState},
 };
 
 #[cfg(feature = "lyric-finder")]
@@ -49,24 +49,36 @@ fn handle_playback_change_event(
     handler_state: &mut PlayerEventHandlerState,
 ) -> anyhow::Result<()> {
     let player = state.player.read();
-    let (Some(playback), Some(track)) = (
+    let (playback, id, name, duration) = match (
         player.buffered_playback.as_ref(),
-        player.current_playing_track(),
-    ) else {
-        return Ok(());
+        player.currently_playing(),
+    ) {
+        (Some(playback), Some(PlayableItem::Track(track))) => (
+            playback,
+            PlayableId::Track(track.id.clone().expect("null track_id")),
+            &track.name,
+            track.duration,
+        ),
+        (Some(playback), Some(PlayableItem::Episode(episode))) => (
+            playback,
+            PlayableId::Episode(episode.id.clone()),
+            &episode.name,
+            episode.duration,
+        ),
+        _ => return Ok(()),
     };
 
     if let Some(progress) = player.playback_progress() {
         // update the playback when the current track ends
-        if progress >= track.duration && playback.is_playing {
+        if progress >= duration && playback.is_playing {
             client_pub.send(ClientRequest::GetCurrentPlayback)?;
         }
     }
 
     if let Some(queue) = player.queue.as_ref() {
         // queue needs to be updated if its playing track is different from actual playback's playing track
-        if let Some(PlayableItem::Track(queue_track)) = queue.currently_playing.as_ref() {
-            if queue_track.id != track.id {
+        if let Some(queue_track) = queue.currently_playing.as_ref() {
+            if queue_track.id().expect("null track_id") != id {
                 client_pub.send(ClientRequest::GetCurrentUserQueue)?;
             }
         }
@@ -79,16 +91,16 @@ fn handle_playback_change_event(
         if let Some(progress) = player.playback_progress() {
             // re-queue the current track if it's about to end while
             // ensuring that only one `AddTrackToQueue` request is made
-            if progress + chrono::TimeDelta::seconds(5) >= track.duration
+            if progress + chrono::TimeDelta::seconds(5) >= duration
                 && playback.is_playing
                 && handler_state.add_track_to_queue_req_timer.elapsed()
                     > std::time::Duration::from_secs(10)
             {
                 tracing::info!(
                     "fake track repeat mode is enabled, add the current track ({}) to queue",
-                    track.name
+                    name
                 );
-                client_pub.send(ClientRequest::AddTrackToQueue(track.id.clone().unwrap()))?;
+                client_pub.send(ClientRequest::AddPlayableToQueue(id))?;
                 handler_state.add_track_to_queue_req_timer = std::time::Instant::now();
             }
         }
@@ -126,6 +138,7 @@ fn handle_page_change_event(
                             ContextId::Artist(_) => ContextPageUIState::new_artist(),
                             ContextId::Playlist(_) => ContextPageUIState::new_playlist(),
                             ContextId::Tracks(_) => ContextPageUIState::new_tracks(),
+                            ContextId::Show(_) => ContextPageUIState::new_show(),
                         });
                     }
                     None => {
@@ -150,7 +163,9 @@ fn handle_page_change_event(
             artists,
             scroll_offset,
         } => {
-            if let Some(current_track) = state.player.read().current_playing_track() {
+            if let Some(rspotify::model::PlayableItem::Track(current_track)) =
+                state.player.read().currently_playing()
+            {
                 if current_track.name != *track {
                     tracing::info!("Current playing track \"{}\" is different from the track \"{track}\" shown up in the lyric page. Updating the track and fetching its lyric...", current_track.name);
                     track.clone_from(&current_track.name);
diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs
index 92527c27..d40c0f14 100644
--- a/spotify_player/src/client/mod.rs
+++ b/spotify_player/src/client/mod.rs
@@ -3,6 +3,7 @@ use std::{borrow::Cow, collections::HashMap, sync::Arc};
 
 #[cfg(feature = "streaming")]
 use crate::state::Mutex;
+use crate::state::Show;
 use crate::{auth, config};
 use crate::{
     auth::AuthConfig,
@@ -10,7 +11,7 @@ use crate::{
         rspotify_model, store_data_into_file_cache, Album, AlbumId, Artist, ArtistId, Category,
         Context, ContextId, Device, FileCacheKey, Item, ItemId, MemoryCaches, Playback,
         PlaybackMetadata, Playlist, PlaylistFolderItem, PlaylistId, SearchResults, SharedState,
-        Track, TrackId, UserId, TTL_CACHE_DURATION, USER_LIKED_TRACKS_ID,
+        ShowId, Track, TrackId, UserId, TTL_CACHE_DURATION, USER_LIKED_TRACKS_ID,
         USER_RECENTLY_PLAYED_TRACKS_ID, USER_TOP_TRACKS_ID,
     },
 };
@@ -20,6 +21,7 @@ use std::io::Write;
 use anyhow::Context as _;
 use anyhow::Result;
 use reqwest::StatusCode;
+use rspotify::model::{AdditionalType, CurrentPlaybackContext};
 use rspotify::{
     http::Query,
     model::{FullPlaylist, Market, Page, SimplifiedPlaylist},
@@ -35,6 +37,7 @@ pub use request::*;
 use serde::Deserialize;
 
 const SPOTIFY_API_ENDPOINT: &str = "https://api.spotify.com/v1";
+const PLAYBACK_TYPES: [&AdditionalType; 2] = [&AdditionalType::Track, &AdditionalType::Episode];
 
 /// The application's Spotify client
 #[derive(Clone)]
@@ -408,6 +411,16 @@ impl Client {
                 .context("store user's saved albums into the cache folder")?;
                 state.data.write().user_data.saved_albums = albums;
             }
+            ClientRequest::GetUserSavedShows => {
+                let shows = self.current_user_saved_shows().await?;
+                store_data_into_file_cache(
+                    FileCacheKey::SavedShows,
+                    &config::get_config().cache_folder,
+                    &shows,
+                )
+                .context("store user's saved shows into the cache folder")?;
+                state.data.write().user_data.saved_shows = shows;
+            }
             ClientRequest::GetUserTopTracks => {
                 let uri = &USER_TOP_TRACKS_ID.uri;
                 if !state.data.read().caches.context.contains_key(uri) {
@@ -474,6 +487,7 @@ impl Client {
                                 "`GetContext` request for `tracks` context is not supported!"
                             );
                         }
+                        ContextId::Show(show_id) => self.show_context(show_id).await?,
                     };
 
                     state
@@ -514,12 +528,11 @@ impl Client {
                     );
                 }
             }
-            ClientRequest::AddTrackToQueue(track_id) => {
-                self.add_item_to_queue(PlayableId::Track(track_id), None)
-                    .await?;
+            ClientRequest::AddPlayableToQueue(playable_id) => {
+                self.add_item_to_queue(playable_id, None).await?;
             }
-            ClientRequest::AddTrackToPlaylist(playlist_id, track_id) => {
-                self.add_track_to_playlist(state, playlist_id, track_id)
+            ClientRequest::AddPlayableToPlaylist(playlist_id, playable_id) => {
+                self.add_item_to_playlist(state, playlist_id, playable_id)
                     .await?;
             }
             ClientRequest::AddAlbumToQueue(album_id) => {
@@ -790,6 +803,13 @@ impl Client {
         Ok(albums.into_iter().map(|a| a.album.into()).collect())
     }
 
+    /// Get all saved shows of the current user
+    pub async fn current_user_saved_shows(&self) -> Result<Vec<Show>> {
+        let first_page = self.get_saved_show_manual(Some(50), None).await?;
+        let shows = self.all_paging_items(first_page, &Query::new()).await?;
+        Ok(shows.into_iter().map(|s| s.show.into()).collect())
+    }
+
     /// Get all albums of an artist
     pub async fn artist_albums(&self, artist_id: ArtistId<'_>) -> Result<Vec<Album>> {
         let payload = market_query();
@@ -847,15 +867,13 @@ impl Client {
                 ContextId::Tracks(_) => {
                     anyhow::bail!("`StartPlayback` request for `tracks` context is not supported")
                 }
+                ContextId::Show(_) => {
+                    anyhow::bail!("`StartPlayback` request for `show` context is not supported")
+                }
             },
-            Playback::URIs(track_ids, offset) => {
-                self.start_uris_playback(
-                    track_ids.into_iter().map(PlayableId::from),
-                    device_id,
-                    offset,
-                    None,
-                )
-                .await?;
+            Playback::URIs(ids, offset) => {
+                self.start_uris_playback(ids, device_id, offset, None)
+                    .await?;
             }
         }
 
@@ -922,14 +940,23 @@ impl Client {
 
     /// Search for items (tracks, artists, albums, playlists) matching a given query
     pub async fn search(&self, query: &str) -> Result<SearchResults> {
-        let (track_result, artist_result, album_result, playlist_result) = tokio::try_join!(
+        let (
+            track_result,
+            artist_result,
+            album_result,
+            playlist_result,
+            show_result,
+            episode_result,
+        ) = tokio::try_join!(
             self.search_specific_type(query, rspotify_model::SearchType::Track),
             self.search_specific_type(query, rspotify_model::SearchType::Artist),
             self.search_specific_type(query, rspotify_model::SearchType::Album),
-            self.search_specific_type(query, rspotify_model::SearchType::Playlist)
+            self.search_specific_type(query, rspotify_model::SearchType::Playlist),
+            self.search_specific_type(query, rspotify_model::SearchType::Show),
+            self.search_specific_type(query, rspotify_model::SearchType::Episode)
         )?;
 
-        let (tracks, artists, albums, playlists) = (
+        let (tracks, artists, albums, playlists, shows, episodes) = (
             match track_result {
                 rspotify_model::SearchResult::Tracks(p) => p
                     .items
@@ -958,6 +985,18 @@ impl Client {
                 }
                 _ => anyhow::bail!("expect a playlist search result"),
             },
+            match show_result {
+                rspotify_model::SearchResult::Shows(p) => {
+                    p.items.into_iter().map(std::convert::Into::into).collect()
+                }
+                _ => anyhow::bail!("expect a show search result"),
+            },
+            match episode_result {
+                rspotify_model::SearchResult::Episodes(p) => {
+                    p.items.into_iter().map(std::convert::Into::into).collect()
+                }
+                _ => anyhow::bail!("expect a episode search result"),
+            },
         );
 
         Ok(SearchResults {
@@ -965,6 +1004,8 @@ impl Client {
             artists,
             albums,
             playlists,
+            shows,
+            episodes,
         })
     }
 
@@ -981,27 +1022,23 @@ impl Client {
             .await?)
     }
 
-    /// Add a track to a playlist
-    pub async fn add_track_to_playlist(
+    /// Add a playable item to a playlist
+    pub async fn add_item_to_playlist(
         &self,
         state: &SharedState,
         playlist_id: PlaylistId<'_>,
-        track_id: TrackId<'_>,
+        playable_id: PlayableId<'_>,
     ) -> Result<()> {
         // remove all the occurrences of the track to ensure no duplication in the playlist
         self.playlist_remove_all_occurrences_of_items(
             playlist_id.as_ref(),
-            [PlayableId::Track(track_id.as_ref())],
+            [playable_id.as_ref()],
             None,
         )
         .await?;
 
-        self.playlist_add_items(
-            playlist_id.as_ref(),
-            [PlayableId::Track(track_id.as_ref())],
-            None,
-        )
-        .await?;
+        self.playlist_add_items(playlist_id.as_ref(), [playable_id.as_ref()], None)
+            .await?;
 
         // After adding a new track to a playlist, remove the cache of that playlist to force refetching new data
         state.data.write().caches.context.remove(&playlist_id.uri());
@@ -1147,6 +1184,14 @@ impl Client {
                     }
                 }
             }
+            Item::Show(show) => {
+                let follows = self.check_users_saved_shows([show.id.as_ref()]).await?;
+                if !follows[0] {
+                    self.save_shows([show.id.as_ref()]).await?;
+                    // update the in-memory `user_data`
+                    state.data.write().user_data.saved_shows.insert(0, show);
+                }
+            }
         }
         Ok(())
     }
@@ -1189,6 +1234,16 @@ impl Client {
                     });
                 self.playlist_unfollow(id).await?;
             }
+            ItemId::Show(id) => {
+                state
+                    .data
+                    .write()
+                    .user_data
+                    .saved_shows
+                    .retain(|s| s.id != id);
+                self.remove_users_saved_shows([id], Some(Market::FromToken))
+                    .await?;
+            }
         }
         Ok(())
     }
@@ -1314,6 +1369,28 @@ impl Client {
         })
     }
 
+    /// Get a show context data
+    pub async fn show_context(&self, show_id: ShowId<'_>) -> Result<Context> {
+        let show_uri = show_id.uri();
+        tracing::info!("Get show context: {}", show_uri);
+
+        let show = self.get_a_show(show_id, None).await?;
+        let first_page = show.episodes.clone();
+
+        // converts `rspotify_model::FullShow` into `state::Show`
+        let show: Show = show.into();
+
+        // get the show's episodes
+        let episodes = self
+            .all_paging_items(first_page, &Query::new())
+            .await?
+            .into_iter()
+            .map(std::convert::Into::into)
+            .collect::<Vec<_>>();
+
+        Ok(Context::Show { show, episodes })
+    }
+
     /// Make a GET HTTP request to the Spotify server
     async fn http_get<T>(&self, url: &str, payload: &Query<'_>) -> Result<T>
     where
@@ -1402,34 +1479,45 @@ impl Client {
         Ok(items)
     }
 
+    pub async fn current_playback2(&self) -> Result<Option<CurrentPlaybackContext>> {
+        Ok(self.current_playback(None, PLAYBACK_TYPES.into()).await?)
+    }
+
     /// Retrieve the latest playback state
     pub async fn retrieve_current_playback(
         &self,
         state: &SharedState,
         reset_buffered_playback: bool,
     ) -> Result<()> {
-        let new_track = {
+        let new_playback = {
             // update the playback state
-            let playback = self.current_playback(None, None::<Vec<_>>).await?;
+            let playback = self.current_playback2().await?;
+            log::info!("current_playback: {playback:?}");
             let mut player = state.player.write();
 
-            let prev_track_name = player
-                .current_playing_track()
-                .map(|t| t.name.clone())
-                .unwrap_or_default();
+            let prev_item = player.currently_playing();
+
+            let prev_name = match prev_item {
+                Some(rspotify_model::PlayableItem::Track(track)) => track.name.clone(),
+                Some(rspotify_model::PlayableItem::Episode(episode)) => episode.name.clone(),
+                None => String::new(),
+            };
 
             player.playback = playback;
             player.playback_last_updated_time = Some(std::time::Instant::now());
 
-            let curr_track_name = player
-                .current_playing_track()
-                .map(|t| t.name.clone())
-                .unwrap_or_default();
+            let curr_item = player.currently_playing();
 
-            let new_track = prev_track_name != curr_track_name && !curr_track_name.is_empty();
+            let curr_name = match curr_item {
+                Some(rspotify_model::PlayableItem::Track(track)) => track.name.clone(),
+                Some(rspotify_model::PlayableItem::Episode(episode)) => episode.name.clone(),
+                None => String::new(),
+            };
+
+            let new_playback = prev_name != curr_name && !curr_name.is_empty();
             // check if we need to update the buffered playback
             let needs_update = match (&player.buffered_playback, &player.playback) {
-                (Some(bp), Some(p)) => bp.device_id != p.device.id || new_track,
+                (Some(bp), Some(p)) => bp.device_id != p.device.id || new_playback,
                 (None, None) => false,
                 _ => true,
             };
@@ -1451,37 +1539,60 @@ impl Client {
                 });
             }
 
-            new_track
+            new_playback
         };
 
-        if !new_track {
+        if !new_playback {
             return Ok(());
         }
-        self.handle_new_track_event(state).await?;
+        self.handle_new_playback_event(state).await?;
 
         Ok(())
     }
 
     // Handle new track event
-    async fn handle_new_track_event(&self, state: &SharedState) -> Result<()> {
+    async fn handle_new_playback_event(&self, state: &SharedState) -> Result<()> {
         let configs = config::get_config();
 
-        let track = match state.player.read().current_playing_track() {
-            None => return Ok(()),
-            Some(track) => track.clone(),
+        let curr_item = {
+            let player = state.player.read();
+            let Some(track_or_episode) = player.currently_playing() else {
+                return Ok(());
+            };
+            track_or_episode.clone()
         };
 
-        let Some(url) = crate::utils::get_track_album_image_url(&track) else {
-            return Ok(());
+        let url = match curr_item {
+            rspotify_model::PlayableItem::Track(ref track) => {
+                crate::utils::get_track_album_image_url(track)
+                    .ok_or(anyhow::anyhow!("missing image"))?
+            }
+            rspotify_model::PlayableItem::Episode(ref episode) => {
+                crate::utils::get_episode_show_image_url(episode)
+                    .ok_or(anyhow::anyhow!("missing image"))?
+            }
         };
 
-        let filename = format!(
-            "{}-{}-cover-{}.jpg",
-            track.album.name,
-            track.album.artists.first().unwrap().name,
-            // first 6 characters of the album's id
-            &track.album.id.as_ref().unwrap().id()[..6]
-        )
+        let filename = (match curr_item {
+            rspotify_model::PlayableItem::Track(ref track) => {
+                format!(
+                    "{}-{}-cover-{}.jpg",
+                    track.album.name,
+                    track.album.artists.first().unwrap().name,
+                    // first 6 characters of the album's id
+                    &track.album.id.as_ref().unwrap().id()[..6]
+                )
+            }
+            rspotify_model::PlayableItem::Episode(ref episode) => {
+                format!(
+                    "{}-{}-cover-{}.jpg",
+                    episode.show.name,
+                    episode.show.publisher,
+                    // first 6 characters of the show's id
+                    &episode.show.id.as_ref().id()[..6]
+                )
+            }
+        })
         .replace('/', ""); // remove invalid characters from the file's name
         let path = configs.cache_folder.join("image").join(filename);
 
@@ -1507,7 +1618,7 @@ impl Client {
         if configs.app_config.enable_notify
             && (!configs.app_config.notify_streaming_only || self.stream_conn.lock().is_some())
         {
-            Self::notify_new_track(&track, &path)?;
+            Self::notify_new_playback(&curr_item, &path)?;
         }
 
         #[cfg(all(feature = "notify", not(feature = "streaming")))]
@@ -1553,9 +1664,9 @@ impl Client {
     }
 
     #[cfg(feature = "notify")]
-    /// Create a notification for a new track
-    fn notify_new_track(
-        track: &rspotify_model::FullTrack,
+    /// Create a notification for a new playback
+    fn notify_new_playback(
+        playable: &rspotify_model::PlayableItem,
         cover_img_path: &std::path::Path,
     ) -> Result<()> {
         let mut n = notify_rust::Notification::new();
@@ -1577,11 +1688,24 @@ impl Client {
                 }
                 ptr = e;
                 match m.as_str() {
-                    "{track}" => text += &track.name,
+                    "{track}" => {
+                        let name = match playable {
+                            rspotify_model::PlayableItem::Track(ref track) => &track.name,
+                            rspotify_model::PlayableItem::Episode(ref episode) => &episode.name,
+                        };
+                        text += name;
+                    }
                     "{artists}" => {
-                        text += &crate::utils::map_join(&track.artists, |a| &a.name, ", ");
+                        if let rspotify_model::PlayableItem::Track(ref track) = playable {
+                            text += &crate::utils::map_join(&track.artists, |a| &a.name, ", ");
+                        }
                     }
-                    "{album}" => text += &track.album.name,
+                    "{album}" => match playable {
+                        rspotify_model::PlayableItem::Track(ref track) => text += &track.album.name,
+                        rspotify_model::PlayableItem::Episode(ref episode) => {
+                            text += &episode.show.name;
+                        }
+                    },
                     _ => continue,
                 }
             }
diff --git a/spotify_player/src/client/request.rs b/spotify_player/src/client/request.rs
index 4b9bef78..3aef69d5 100644
--- a/spotify_player/src/client/request.rs
+++ b/spotify_player/src/client/request.rs
@@ -1,4 +1,6 @@
-use crate::state::{AlbumId, Category, ContextId, Item, ItemId, Playback, PlaylistId, TrackId};
+use crate::state::{
+    AlbumId, Category, ContextId, Item, ItemId, PlayableId, Playback, PlaylistId, TrackId,
+};
 
 #[derive(Clone, Debug)]
 /// A request that modifies the player's playback
@@ -26,6 +28,7 @@ pub enum ClientRequest {
     GetBrowseCategoryPlaylists(Category),
     GetUserPlaylists,
     GetUserSavedAlbums,
+    GetUserSavedShows,
     GetUserFollowedArtists,
     GetUserSavedTracks,
     GetUserTopTracks,
@@ -37,9 +40,9 @@ pub enum ClientRequest {
         seed_name: String,
     },
     Search(String),
-    AddTrackToQueue(TrackId<'static>),
+    AddPlayableToQueue(PlayableId<'static>),
     AddAlbumToQueue(AlbumId<'static>),
-    AddTrackToPlaylist(PlaylistId<'static>, TrackId<'static>),
+    AddPlayableToPlaylist(PlaylistId<'static>, PlayableId<'static>),
     DeleteTrackFromPlaylist(PlaylistId<'static>, TrackId<'static>),
     ReorderPlaylistItems {
         playlist_id: PlaylistId<'static>,
diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs
index 428d5f39..d8625fcc 100644
--- a/spotify_player/src/command.rs
+++ b/spotify_player/src/command.rs
@@ -1,5 +1,6 @@
 use crate::state::{
-    Album, Artist, DataReadGuard, Playlist, PlaylistFolder, PlaylistFolderItem, Track,
+    Album, Artist, DataReadGuard, Episode, Playlist, PlaylistFolder, PlaylistFolderItem, Show,
+    Track,
 };
 use serde::Deserialize;
 
@@ -86,6 +87,7 @@ pub enum Action {
     GoToArtist,
     GoToAlbum,
     GoToRadio,
+    GoToShow,
     AddToLibrary,
     AddToPlaylist,
     AddToQueue,
@@ -95,6 +97,7 @@ pub enum Action {
     DeleteFromPlaylist,
     ShowActionsOnAlbum,
     ShowActionsOnArtist,
+    ShowActionsOnShow,
     ToggleLiked,
     CopyLink,
     Follow,
@@ -107,9 +110,11 @@ pub enum ActionContext {
     Album(Album),
     Artist(Artist),
     Playlist(Playlist),
+    Episode(Episode),
     #[allow(dead_code)]
     // TODO: support actions for playlist folders
     PlaylistFolder(PlaylistFolder),
+    Show(Show),
 }
 
 #[derive(Debug, PartialEq, Clone, Deserialize, Default, Copy)]
@@ -148,6 +153,18 @@ impl From<Playlist> for ActionContext {
     }
 }
 
+impl From<Show> for ActionContext {
+    fn from(v: Show) -> Self {
+        Self::Show(v)
+    }
+}
+
+impl From<Episode> for ActionContext {
+    fn from(v: Episode) -> Self {
+        Self::Episode(v)
+    }
+}
+
 impl From<PlaylistFolderItem> for ActionContext {
     fn from(value: PlaylistFolderItem) -> Self {
         match value {
@@ -164,8 +181,10 @@ impl ActionContext {
             Self::Album(album) => construct_album_actions(album, data),
             Self::Artist(artist) => construct_artist_actions(artist, data),
             Self::Playlist(playlist) => construct_playlist_actions(playlist, data),
+            Self::Episode(episode) => construct_episode_actions(episode, data),
             // TODO: support actions for playlist folders
             Self::PlaylistFolder(_) => vec![],
+            Self::Show(show) => construct_show_actions(show, data),
         }
     }
 }
@@ -243,6 +262,27 @@ pub fn construct_playlist_actions(playlist: &Playlist, data: &DataReadGuard) ->
     actions
 }
 
+/// constructs a list of actions on a show
+pub fn construct_show_actions(show: &Show, data: &DataReadGuard) -> Vec<Action> {
+    let mut actions = vec![Action::CopyLink];
+    if data.user_data.saved_shows.iter().any(|s| s.id == show.id) {
+        actions.push(Action::DeleteFromLibrary);
+    } else {
+        actions.push(Action::AddToLibrary);
+    }
+    actions
+}
+
+/// constructs a list of actions on an episode
+pub fn construct_episode_actions(episode: &Episode, _data: &DataReadGuard) -> Vec<Action> {
+    let mut actions = vec![Action::CopyLink, Action::AddToPlaylist, Action::AddToQueue];
+    if episode.show.is_some() {
+        actions.push(Action::ShowActionsOnShow);
+        actions.push(Action::GoToShow);
+    }
+    actions
+}
+
 impl Command {
     pub fn desc(self) -> &'static str {
         match self {
diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs
index 2b44ffde..1ebdc905 100644
--- a/spotify_player/src/event/mod.rs
+++ b/spotify_player/src/event/mod.rs
@@ -10,9 +10,9 @@ use crate::{
         rspotify_model, ActionListItem, Album, AlbumId, Artist, ArtistFocusState, ArtistId,
         ArtistPopupAction, BrowsePageUIState, Context, ContextId, ContextPageType,
         ContextPageUIState, DataReadGuard, Focusable, Id, Item, ItemId, LibraryFocusState,
-        LibraryPageUIState, PageState, PageType, Playback, PlaylistCreateCurrentField,
+        LibraryPageUIState, PageState, PageType, PlayableId, Playback, PlaylistCreateCurrentField,
         PlaylistFolderItem, PlaylistId, PlaylistPopupAction, PopupState, SearchFocusState,
-        SearchPageUIState, SharedState, Track, TrackId, TrackOrder, UIStateGuard,
+        SearchPageUIState, SharedState, ShowId, Track, TrackId, TrackOrder, UIStateGuard,
         USER_LIKED_TRACKS_ID, USER_RECENTLY_PLAYED_TRACKS_ID, USER_TOP_TRACKS_ID,
     },
     ui::{single_line_input::LineInput, Orientation},
@@ -74,11 +74,12 @@ fn handle_mouse_event(
         if event.row == rect.y {
             // calculate the seek position (in ms) based on the mouse click position,
             // the progress bar's width and the track's duration (in ms)
-            let duration = state
-                .player
-                .read()
-                .current_playing_track()
-                .map(|t| t.duration);
+            let player = state.player.read();
+            let duration = match player.currently_playing() {
+                Some(rspotify_model::PlayableItem::Track(track)) => Some(track.duration),
+                Some(rspotify_model::PlayableItem::Episode(episode)) => Some(episode.duration),
+                None => None,
+            };
             if let Some(duration) = duration {
                 let position_ms =
                     (duration.num_milliseconds()) * i64::from(event.column) / i64::from(rect.width);
@@ -172,7 +173,7 @@ pub fn handle_action_in_context(
                 Ok(true)
             }
             Action::AddToQueue => {
-                client_pub.send(ClientRequest::AddTrackToQueue(track.id))?;
+                client_pub.send(ClientRequest::AddPlayableToQueue(track.id.into()))?;
                 ui.popup = None;
                 Ok(true)
             }
@@ -359,6 +360,78 @@ pub fn handle_action_in_context(
             }
             _ => Ok(false),
         },
+        ActionContext::Show(show) => match action {
+            Action::CopyLink => {
+                let show_url = format!("https://open.spotify.com/show/{}", show.id.id());
+                execute_copy_command(show_url)?;
+                ui.popup = None;
+                Ok(true)
+            }
+            Action::AddToLibrary => {
+                client_pub.send(ClientRequest::AddToLibrary(Item::Show(show)))?;
+                ui.popup = None;
+                Ok(true)
+            }
+            Action::DeleteFromLibrary => {
+                client_pub.send(ClientRequest::DeleteFromLibrary(ItemId::Show(show.id)))?;
+                ui.popup = None;
+                Ok(true)
+            }
+            _ => Ok(false),
+        },
+        ActionContext::Episode(episode) => match action {
+            Action::GoToShow => {
+                if let Some(show) = episode.show {
+                    let context_id = ContextId::Show(
+                        ShowId::from_uri(&parse_uri(&show.id.uri()))?.into_static(),
+                    );
+                    ui.new_page(PageState::Context {
+                        id: None,
+                        context_page_type: ContextPageType::Browsing(context_id),
+                        state: None,
+                    });
+                    return Ok(true);
+                }
+                Ok(false)
+            }
+            Action::AddToQueue => {
+                client_pub.send(ClientRequest::AddPlayableToQueue(episode.id.into()))?;
+                ui.popup = None;
+                Ok(true)
+            }
+            Action::CopyLink => {
+                let episode_url = format!("https://open.spotify.com/episode/{}", episode.id.id());
+                execute_copy_command(episode_url)?;
+                ui.popup = None;
+                Ok(true)
+            }
+            Action::AddToPlaylist => {
+                client_pub.send(ClientRequest::GetUserPlaylists)?;
+                ui.popup = Some(PopupState::UserPlaylistList(
+                    PlaylistPopupAction::AddEpisode {
+                        folder_id: 0,
+                        episode_id: episode.id,
+                    },
+                    ListState::default(),
+                ));
+                Ok(true)
+            }
+            Action::ShowActionsOnShow => {
+                if let Some(show) = episode.show {
+                    let context = ActionContext::Show(show.clone());
+                    ui.popup = Some(PopupState::ActionList(
+                        Box::new(ActionListItem::Show(
+                            show,
+                            context.get_available_actions(data),
+                        )),
+                        ListState::default(),
+                    ));
+                    return Ok(true);
+                }
+                Ok(false)
+            }
+            _ => Ok(false),
+        },
         // TODO: support actions for playlist folders
         ActionContext::PlaylistFolder(_) => Ok(false),
     }
@@ -414,15 +487,28 @@ fn handle_global_action(
         let player = state.player.read();
         let data = state.data.read();
 
-        if let Some(currently_playing) = player.current_playing_track() {
-            if let Some(track) = Track::try_from_full_track(currently_playing.clone()) {
-                return handle_action_in_context(
-                    action,
-                    ActionContext::Track(track),
-                    client_pub,
-                    &data,
-                    ui,
-                );
+        if let Some(currently_playing) = player.currently_playing() {
+            match currently_playing {
+                rspotify_model::PlayableItem::Track(track) => {
+                    if let Some(track) = Track::try_from_full_track(track.clone()) {
+                        return handle_action_in_context(
+                            action,
+                            ActionContext::Track(track),
+                            client_pub,
+                            &data,
+                            ui,
+                        );
+                    }
+                }
+                rspotify_model::PlayableItem::Episode(episode) => {
+                    return handle_action_in_context(
+                        action,
+                        ActionContext::Episode(episode.clone().into()),
+                        client_pub,
+                        &data,
+                        ui,
+                    );
+                }
             }
         }
     };
@@ -507,14 +593,27 @@ fn handle_global_command(
             client_pub.send(ClientRequest::GetCurrentPlayback)?;
         }
         Command::ShowActionsOnCurrentTrack => {
-            if let Some(track) = state.player.read().current_playing_track() {
-                if let Some(track) = Track::try_from_full_track(track.clone()) {
-                    let data = state.data.read();
-                    let actions = command::construct_track_actions(&track, &data);
-                    ui.popup = Some(PopupState::ActionList(
-                        Box::new(ActionListItem::Track(track, actions)),
-                        ListState::default(),
-                    ));
+            if let Some(currently_playing) = state.player.read().currently_playing() {
+                match currently_playing {
+                    rspotify_model::PlayableItem::Track(track) => {
+                        if let Some(track) = Track::try_from_full_track(track.clone()) {
+                            let data = state.data.read();
+                            let actions = command::construct_track_actions(&track, &data);
+                            ui.popup = Some(PopupState::ActionList(
+                                Box::new(ActionListItem::Track(track, actions)),
+                                ListState::default(),
+                            ));
+                        }
+                    }
+                    rspotify_model::PlayableItem::Episode(episode) => {
+                        let episode = episode.clone().into();
+                        let data = state.data.read();
+                        let actions = command::construct_episode_actions(&episode, &data);
+                        ui.popup = Some(PopupState::ActionList(
+                            Box::new(ActionListItem::Episode(episode, actions)),
+                            ListState::default(),
+                        ));
+                    }
                 }
             }
         }
@@ -609,7 +708,7 @@ fn handle_global_command(
                     "track" => {
                         let id = TrackId::from_id(id)?.into_static();
                         client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback(
-                            Playback::URIs(vec![id], None),
+                            Playback::URIs(vec![id.into()], None),
                             None,
                         )))?;
                     }
@@ -646,7 +745,9 @@ fn handle_global_command(
         }
         #[cfg(feature = "lyric-finder")]
         Command::LyricPage => {
-            if let Some(track) = state.player.read().current_playing_track() {
+            if let Some(rspotify_model::PlayableItem::Track(track)) =
+                state.player.read().currently_playing()
+            {
                 let artists = map_join(&track.artists, |a| &a.name, ", ");
                 ui.new_page(PageState::Lyric {
                     track: track.name.clone(),
@@ -701,13 +802,14 @@ fn handle_global_command(
             });
         }
         Command::JumpToCurrentTrackInContext => {
-            let Some(track_id) = state
-                .player
-                .read()
-                .current_playing_track()
-                .and_then(|track| track.id.clone())
-            else {
-                return Ok(false);
+            let track_id = match state.player.read().currently_playing() {
+                Some(rspotify_model::PlayableItem::Track(track)) => {
+                    PlayableId::Track(track.id.clone().expect("all non-local tracks have ids"))
+                }
+                Some(rspotify_model::PlayableItem::Episode(episode)) => {
+                    PlayableId::Episode(episode.id.clone())
+                }
+                None => return Ok(false),
             };
 
             if let PageState::Context {
@@ -719,7 +821,7 @@ fn handle_global_command(
                     .data
                     .read()
                     .context_tracks(context_id)
-                    .and_then(|tracks| tracks.iter().position(|t| t.id == track_id));
+                    .and_then(|tracks| tracks.iter().position(|t| t.id.uri() == track_id.uri()));
 
                 if let Some(p) = context_track_pos {
                     ui.current_page_mut().select(p);
diff --git a/spotify_player/src/event/page.rs b/spotify_player/src/event/page.rs
index 25fb0386..68e9302c 100644
--- a/spotify_player/src/event/page.rs
+++ b/spotify_player/src/event/page.rs
@@ -248,6 +248,39 @@ fn handle_key_sequence_for_search_page(
                 CommandOrAction::Action(..) => Ok(false),
             }
         }
+        SearchFocusState::Shows => {
+            let shows = search_results
+                .map(|s| s.shows.iter().collect::<Vec<_>>())
+                .unwrap_or_default();
+
+            match found_keymap {
+                CommandOrAction::Command(command) => Ok(
+                    window::handle_command_for_show_list_window(command, &shows, &data, ui),
+                ),
+                CommandOrAction::Action(action, ActionTarget::SelectedItem) => {
+                    window::handle_action_for_selected_item(action, shows, &data, ui, client_pub)
+                }
+                CommandOrAction::Action(..) => Ok(false),
+            }
+        }
+        SearchFocusState::Episodes => {
+            let episodes = match search_results {
+                Some(s) => s.episodes.iter().collect(),
+                None => Vec::new(),
+            };
+
+            match found_keymap {
+                CommandOrAction::Command(command) => {
+                    window::handle_command_for_episode_list_window(
+                        command, client_pub, &episodes, &data, ui,
+                    )
+                }
+                CommandOrAction::Action(action, ActionTarget::SelectedItem) => {
+                    window::handle_action_for_selected_item(action, episodes, &data, ui, client_pub)
+                }
+                CommandOrAction::Action(..) => Ok(false),
+            }
+        }
     }
 }
 
diff --git a/spotify_player/src/event/popup.rs b/spotify_player/src/event/popup.rs
index b786ea90..215c31c2 100644
--- a/spotify_player/src/event/popup.rs
+++ b/spotify_player/src/event/popup.rs
@@ -97,7 +97,7 @@ pub fn handle_key_sequence_for_popup(
                     items.len(),
                     |_, _| {},
                     |ui: &mut UIStateGuard, id: usize| -> Result<()> {
-                        match items.get(id).context("invalid index")? {
+                        match items.get(id).expect("invalid index") {
                             PlaylistFolderItem::Folder(f) => {
                                 ui.popup = Some(PopupState::UserPlaylistList(
                                     PlaylistPopupAction::Browse {
@@ -139,7 +139,7 @@ pub fn handle_key_sequence_for_popup(
                     items.len(),
                     |_, _| {},
                     |ui: &mut UIStateGuard, id: usize| -> Result<()> {
-                        ui.popup = match items.get(id).context("invalid index")? {
+                        ui.popup = match items.get(id).expect("invalid index") {
                             PlaylistFolderItem::Folder(f) => Some(PopupState::UserPlaylistList(
                                 PlaylistPopupAction::AddTrack {
                                     folder_id: f.target_id,
@@ -148,9 +148,46 @@ pub fn handle_key_sequence_for_popup(
                                 ListState::default(),
                             )),
                             PlaylistFolderItem::Playlist(p) => {
-                                client_pub.send(ClientRequest::AddTrackToPlaylist(
+                                client_pub.send(ClientRequest::AddPlayableToPlaylist(
                                     p.id.clone(),
-                                    track_id,
+                                    track_id.into(),
+                                ))?;
+                                None
+                            }
+                        };
+                        Ok(())
+                    },
+                    |ui: &mut UIStateGuard| {
+                        ui.popup = None;
+                    },
+                )
+            }
+            PlaylistPopupAction::AddEpisode {
+                folder_id,
+                episode_id,
+            } => {
+                let episode_id = episode_id.clone();
+                let data = state.data.read();
+                let items = data.user_data.modifiable_playlist_items(Some(*folder_id));
+
+                handle_command_for_list_popup(
+                    command,
+                    ui,
+                    items.len(),
+                    |_, _| {},
+                    |ui: &mut UIStateGuard, id: usize| -> Result<()> {
+                        ui.popup = match items.get(id).expect("invalid index") {
+                            PlaylistFolderItem::Folder(f) => Some(PopupState::UserPlaylistList(
+                                PlaylistPopupAction::AddEpisode {
+                                    folder_id: f.target_id,
+                                    episode_id,
+                                },
+                                ListState::default(),
+                            )),
+                            PlaylistFolderItem::Playlist(p) => {
+                                client_pub.send(ClientRequest::AddPlayableToPlaylist(
+                                    p.id.clone(),
+                                    episode_id.into(),
                                 ))?;
                                 None
                             }
@@ -353,7 +390,7 @@ fn handle_command_for_context_browsing_list_popup(
         uris.len(),
         |_, _| {},
         |ui: &mut UIStateGuard, id: usize| -> Result<()> {
-            let uri = crate::utils::parse_uri(uris.get(id).context("invalid index")?);
+            let uri = crate::utils::parse_uri(&uris[id]);
             let context_id = match context_type {
                 rspotify_model::Type::Playlist => {
                     ContextId::Playlist(PlaylistId::from_uri(&uri)?.into_static())
@@ -493,5 +530,11 @@ pub fn handle_item_action(
         ActionListItem::Playlist(playlist, actions) => {
             handle_action_in_context(actions[n], playlist.into(), client_pub, &data, ui)
         }
+        ActionListItem::Show(show, actions) => {
+            handle_action_in_context(actions[n], show.into(), client_pub, &data, ui)
+        }
+        ActionListItem::Episode(episode, actions) => {
+            handle_action_in_context(actions[n], episode.into(), client_pub, &data, ui)
+        }
     }
 }
diff --git a/spotify_player/src/event/window.rs b/spotify_player/src/event/window.rs
index ac88acf5..52d81b81 100644
--- a/spotify_player/src/event/window.rs
+++ b/spotify_player/src/event/window.rs
@@ -1,8 +1,11 @@
 use super::page::handle_navigation_command;
 use super::*;
 use crate::{
-    command::{construct_album_actions, construct_artist_actions, construct_playlist_actions},
-    state::UIStateGuard,
+    command::{
+        construct_album_actions, construct_artist_actions, construct_playlist_actions,
+        construct_show_actions,
+    },
+    state::{Episode, Show, UIStateGuard},
 };
 use command::Action;
 use rand::Rng;
@@ -68,6 +71,13 @@ pub fn handle_action_for_focused_context_page(
             ui,
             client_pub,
         ),
+        Some(Context::Show { episodes, .. }) => handle_action_for_selected_item(
+            action,
+            ui.search_filtered_items(episodes),
+            &data,
+            ui,
+            client_pub,
+        ),
         None => Ok(false),
     }
 }
@@ -182,6 +192,10 @@ pub fn handle_command_for_focused_context_window(
                 &data,
                 ui,
             ),
+            Context::Show { .. } => {
+                // TODO: implement command handler for episode table window similar to track table window
+                Ok(false)
+            }
         },
         None => Ok(false),
     }
@@ -290,7 +304,7 @@ fn handle_command_for_track_table_window(
             let base_playback = if let Some(context_id) = context_id {
                 Playback::Context(context_id, None)
             } else {
-                Playback::URIs(tracks.iter().map(|t| t.id.clone_static()).collect(), None)
+                Playback::URIs(tracks.iter().map(|t| t.id.clone().into()).collect(), None)
             };
 
             client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback(
@@ -307,8 +321,8 @@ fn handle_command_for_track_table_window(
             ));
         }
         Command::AddSelectedItemToQueue => {
-            client_pub.send(ClientRequest::AddTrackToQueue(
-                filtered_tracks[id].id.clone(),
+            client_pub.send(ClientRequest::AddPlayableToQueue(
+                filtered_tracks[id].id.clone().into(),
             ))?;
         }
         _ => return Ok(false),
@@ -340,7 +354,7 @@ pub fn handle_command_for_track_list_window(
             // `ChooseSelected` by starting a `URIs` playback
             // containing all the tracks in the table.
             client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback(
-                Playback::URIs(vec![tracks[id].id.clone()], None),
+                Playback::URIs(vec![tracks[id].id.clone().into()], None),
                 None,
             )))?;
         }
@@ -352,7 +366,9 @@ pub fn handle_command_for_track_list_window(
             ));
         }
         Command::AddSelectedItemToQueue => {
-            client_pub.send(ClientRequest::AddTrackToQueue(tracks[id].id.clone()))?;
+            client_pub.send(ClientRequest::AddPlayableToQueue(
+                tracks[id].id.clone().into(),
+            ))?;
         }
         _ => return Ok(false),
     }
@@ -491,3 +507,77 @@ pub fn handle_command_for_playlist_list_window(
     }
     Ok(true)
 }
+
+pub fn handle_command_for_show_list_window(
+    command: Command,
+    shows: &[&Show],
+    data: &DataReadGuard,
+    ui: &mut UIStateGuard,
+) -> bool {
+    let id = ui.current_page_mut().selected().unwrap_or_default();
+    if id >= shows.len() {
+        return false;
+    }
+
+    if handle_navigation_command(command, ui.current_page_mut(), id, shows.len()) {
+        return true;
+    }
+    match command {
+        Command::ChooseSelected => {
+            let context_id = ContextId::Show(shows[id].id.clone());
+            ui.new_page(PageState::Context {
+                id: None,
+                context_page_type: ContextPageType::Browsing(context_id),
+                state: None,
+            });
+        }
+        Command::ShowActionsOnSelectedItem => {
+            let actions = construct_show_actions(shows[id], data);
+            ui.popup = Some(PopupState::ActionList(
+                Box::new(ActionListItem::Show(shows[id].clone(), actions)),
+                ListState::default(),
+            ));
+        }
+        _ => return false,
+    }
+    true
+}
+
+pub fn handle_command_for_episode_list_window(
+    command: Command,
+    client_pub: &flume::Sender<ClientRequest>,
+    episodes: &[&Episode],
+    data: &DataReadGuard,
+    ui: &mut UIStateGuard,
+) -> Result<bool> {
+    let id = ui.current_page_mut().selected().unwrap_or_default();
+    if id >= episodes.len() {
+        return Ok(false);
+    }
+
+    if handle_navigation_command(command, ui.current_page_mut(), id, episodes.len()) {
+        return Ok(true);
+    }
+    match command {
+        Command::ChooseSelected => {
+            client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback(
+                Playback::URIs(vec![episodes[id].id.clone().into()], None),
+                None,
+            )))?;
+        }
+        Command::ShowActionsOnSelectedItem => {
+            let actions = command::construct_episode_actions(episodes[id], data);
+            ui.popup = Some(PopupState::ActionList(
+                Box::new(ActionListItem::Episode(episodes[id].clone(), actions)),
+                ListState::default(),
+            ));
+        }
+        Command::AddSelectedItemToQueue => {
+            client_pub.send(ClientRequest::AddPlayableToQueue(
+                episodes[id].id.clone().into(),
+            ))?;
+        }
+        _ => return Ok(false),
+    }
+    Ok(true)
+}
diff --git a/spotify_player/src/main.rs b/spotify_player/src/main.rs
index a4ff63a0..0c646702 100644
--- a/spotify_player/src/main.rs
+++ b/spotify_player/src/main.rs
@@ -31,6 +31,7 @@ async fn init_spotify(
     client_pub.send(client::ClientRequest::GetUserFollowedArtists)?;
     client_pub.send(client::ClientRequest::GetUserSavedAlbums)?;
     client_pub.send(client::ClientRequest::GetUserSavedTracks)?;
+    client_pub.send(client::ClientRequest::GetUserSavedShows)?;
 
     Ok(())
 }
diff --git a/spotify_player/src/media_control.rs b/spotify_player/src/media_control.rs
index 414783eb..b2a05e5e 100644
--- a/spotify_player/src/media_control.rs
+++ b/spotify_player/src/media_control.rs
@@ -1,4 +1,5 @@
 #![allow(unused_imports)]
+use rspotify::model as rspotify_model;
 use souvlaki::MediaPosition;
 use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig};
 
@@ -12,38 +13,55 @@ use crate::{
 fn update_control_metadata(
     state: &SharedState,
     controls: &mut MediaControls,
-    prev_track_info: &mut String,
+    prev_info: &mut String,
 ) -> Result<(), souvlaki::Error> {
     let player = state.player.read();
 
-    match player.current_playing_track() {
+    match player.currently_playing() {
         None => {}
-        Some(track) => {
-            if let Some(ref playback) = player.playback {
-                let progress = player
-                    .playback_progress()
-                    .and_then(|p| Some(MediaPosition(p.to_std().ok()?)));
-
-                if playback.is_playing {
-                    controls.set_playback(MediaPlayback::Playing { progress })?;
-                } else {
-                    controls.set_playback(MediaPlayback::Paused { progress })?;
-                }
-            }
+        Some(item) => {
+            let progress = player
+                .playback_progress()
+                .and_then(|p| Some(MediaPosition(p.to_std().ok()?)));
 
-            // only update metadata when the track information is changed
-            let track_info = format!("{}/{}", track.name, track.album.name);
-            if track_info != *prev_track_info {
-                controls.set_metadata(MediaMetadata {
-                    title: Some(&track.name),
-                    album: Some(&track.album.name),
-                    artist: Some(&map_join(&track.artists, |a| &a.name, ", ")),
-                    duration: track.duration.to_std().ok(),
-                    cover_url: utils::get_track_album_image_url(track),
-                })?;
-
-                *prev_track_info = track_info;
+            if player.playback.as_ref().expect("playback").is_playing {
+                controls.set_playback(MediaPlayback::Playing { progress })?;
+            } else {
+                controls.set_playback(MediaPlayback::Paused { progress })?;
             }
+
+            match item {
+                rspotify_model::PlayableItem::Track(track) => {
+                    // only update metadata when the track information is changed
+                    let track_info = format!("{}/{}", track.name, track.album.name);
+                    if track_info != *prev_info {
+                        controls.set_metadata(MediaMetadata {
+                            title: Some(&track.name),
+                            album: Some(&track.album.name),
+                            artist: Some(&map_join(&track.artists, |a| &a.name, ", ")),
+                            duration: track.duration.to_std().ok(),
+                            cover_url: utils::get_track_album_image_url(track),
+                        })?;
+
+                        *prev_info = track_info;
+                    }
+                }
+                rspotify_model::PlayableItem::Episode(episode) => {
+                    // only update metadata when the episode information is changed
+                    let episode_info = format!("{}/{}", episode.name, episode.show.name);
+                    if episode_info != *prev_info {
+                        controls.set_metadata(MediaMetadata {
+                            title: Some(&episode.name),
+                            album: Some(&episode.show.name),
+                            artist: Some(&episode.show.publisher),
+                            duration: episode.duration.to_std().ok(),
+                            cover_url: utils::get_episode_show_image_url(episode),
+                        })?;
+
+                        *prev_info = episode_info;
+                    }
+                }
+            };
         }
     }
 
@@ -125,9 +143,9 @@ pub fn start_event_watcher(
     // handler provided by the souvlaki library, which only handles an event every 1s.
     // [1]: https://github.com/Sinono3/souvlaki/blob/b4d47bb2797ffdd625c17192df640510466762e1/src/platform/linux/mod.rs#L450
     let refresh_duration = std::time::Duration::from_millis(1000);
-    let mut track_info = String::new();
+    let mut info = String::new();
     loop {
-        update_control_metadata(state, &mut controls, &mut track_info)?;
+        update_control_metadata(state, &mut controls, &mut info)?;
         std::thread::sleep(refresh_duration);
 
         // this must be run repeatedly to ensure that
diff --git a/spotify_player/src/state/data.rs b/spotify_player/src/state/data.rs
index 1776a9d9..750a5055 100644
--- a/spotify_player/src/state/data.rs
+++ b/spotify_player/src/state/data.rs
@@ -6,7 +6,7 @@ use serde::{de::DeserializeOwned, Serialize};
 
 use super::model::{
     rspotify_model, Album, Artist, Category, Context, ContextId, Id, Playlist, PlaylistFolderItem,
-    PlaylistFolderNode, SearchResults, Track,
+    PlaylistFolderNode, SearchResults, Show, Track,
 };
 
 pub type DataReadGuard<'a> = parking_lot::RwLockReadGuard<'a, AppData>;
@@ -16,6 +16,7 @@ pub enum FileCacheKey {
     Playlists,
     PlaylistFolders,
     FollowedArtists,
+    SavedShows,
     SavedAlbums,
     SavedTracks,
 }
@@ -38,6 +39,7 @@ pub struct UserData {
     pub playlists: Vec<PlaylistFolderItem>,
     pub playlist_folder_node: Option<PlaylistFolderNode>,
     pub followed_artists: Vec<Artist>,
+    pub saved_shows: Vec<Show>,
     pub saved_albums: Vec<Album>,
     pub saved_tracks: HashMap<String, Track>,
 }
@@ -83,17 +85,36 @@ impl AppData {
 
     /// Get a list of tracks inside a given context
     pub fn context_tracks_mut(&mut self, id: &ContextId) -> Option<&mut Vec<Track>> {
-        self.caches
-            .context
-            .get_mut(&id.uri())
-            .map(super::model::Context::tracks_mut)
+        let c = self.caches.context.get_mut(&id.uri())?;
+
+        Some(match c {
+            Context::Album { tracks, .. }
+            | Context::Playlist { tracks, .. }
+            | Context::Tracks { tracks, .. }
+            | Context::Artist {
+                top_tracks: tracks, ..
+            } => tracks,
+            Context::Show { .. } => {
+                // TODO: handle context_tracks_mut for Show
+                return None;
+            }
+        })
     }
 
     pub fn context_tracks(&self, id: &ContextId) -> Option<&Vec<Track>> {
-        self.caches
-            .context
-            .get(&id.uri())
-            .map(super::model::Context::tracks)
+        let c = self.caches.context.get(&id.uri())?;
+        Some(match c {
+            Context::Album { tracks, .. }
+            | Context::Playlist { tracks, .. }
+            | Context::Tracks { tracks, .. }
+            | Context::Artist {
+                top_tracks: tracks, ..
+            } => tracks,
+            Context::Show { .. } => {
+                // TODO: handle context_tracks for Show
+                return None;
+            }
+        })
     }
 }
 
@@ -113,6 +134,8 @@ impl UserData {
                 cache_folder,
             )
             .unwrap_or_default(),
+            saved_shows: load_data_from_file_cache(FileCacheKey::SavedShows, cache_folder)
+                .unwrap_or_default(),
             saved_albums: load_data_from_file_cache(FileCacheKey::SavedAlbums, cache_folder)
                 .unwrap_or_default(),
             saved_tracks: load_data_from_file_cache(FileCacheKey::SavedTracks, cache_folder)
diff --git a/spotify_player/src/state/model.rs b/spotify_player/src/state/model.rs
index 2f6e76de..0b685fc0 100644
--- a/spotify_player/src/state/model.rs
+++ b/spotify_player/src/state/model.rs
@@ -1,6 +1,8 @@
 pub use rspotify::model as rspotify_model;
 use rspotify::model::CurrentPlaybackContext;
-pub use rspotify::model::{AlbumId, AlbumType, ArtistId, Id, PlaylistId, TrackId, UserId};
+pub use rspotify::model::{
+    AlbumId, AlbumType, ArtistId, EpisodeId, Id, PlayableId, PlaylistId, ShowId, TrackId, UserId,
+};
 
 use crate::utils::map_join;
 use html_escape::decode_html_entities;
@@ -29,26 +31,10 @@ pub enum Context {
         tracks: Vec<Track>,
         desc: String,
     },
-}
-
-impl Context {
-    pub fn tracks_mut(&mut self) -> &mut Vec<Track> {
-        match self {
-            Context::Album { tracks, .. }
-            | Context::Playlist { tracks, .. }
-            | Context::Tracks { tracks, .. } => tracks,
-            Context::Artist { top_tracks, .. } => top_tracks,
-        }
-    }
-
-    pub fn tracks(&self) -> &Vec<Track> {
-        match self {
-            Context::Album { tracks, .. }
-            | Context::Playlist { tracks, .. }
-            | Context::Tracks { tracks, .. } => tracks,
-            Context::Artist { top_tracks, .. } => top_tracks,
-        }
-    }
+    Show {
+        show: Show,
+        episodes: Vec<Episode>,
+    },
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -64,18 +50,19 @@ pub enum ContextId {
     Album(AlbumId<'static>),
     Artist(ArtistId<'static>),
     Tracks(TracksId),
+    Show(ShowId<'static>),
 }
 
-#[derive(Clone, Debug)]
 /// Data used to start a new playback.
 /// There are two ways to start a new playback:
 /// - Specify the playing context ID with an offset
 /// - Specify the list of track IDs with an offset
 ///
 /// An offset can be either a track's URI or its absolute offset in the context
+#[derive(Clone, Debug)]
 pub enum Playback {
     Context(ContextId, Option<rspotify_model::Offset>),
-    URIs(Vec<TrackId<'static>>, Option<rspotify_model::Offset>),
+    URIs(Vec<PlayableId<'static>>, Option<rspotify_model::Offset>),
 }
 
 #[derive(Default, Clone, Debug, Deserialize, Serialize)]
@@ -85,6 +72,8 @@ pub struct SearchResults {
     pub artists: Vec<Artist>,
     pub albums: Vec<Album>,
     pub playlists: Vec<Playlist>,
+    pub shows: Vec<Show>,
+    pub episodes: Vec<Episode>,
 }
 
 #[derive(Debug)]
@@ -104,6 +93,7 @@ pub enum Item {
     Album(Album),
     Artist(Artist),
     Playlist(Playlist),
+    Show(Show),
 }
 
 #[derive(Debug, Clone)]
@@ -112,6 +102,7 @@ pub enum ItemId {
     Album(AlbumId<'static>),
     Artist(ArtistId<'static>),
     Playlist(PlaylistId<'static>),
+    Show(ShowId<'static>),
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -180,6 +171,24 @@ pub struct Playlist {
     pub current_folder_id: usize,
 }
 
+#[derive(Deserialize, Serialize, Debug, Clone)]
+/// A Spotify show (podcast)
+pub struct Show {
+    pub id: ShowId<'static>,
+    pub name: String,
+}
+
+#[derive(Deserialize, Serialize, Debug, Clone)]
+/// A Spotify episode (podcast episode)
+pub struct Episode {
+    pub id: EpisodeId<'static>,
+    pub name: String,
+    pub description: String,
+    pub duration: std::time::Duration,
+    pub show: Option<Show>,
+    pub release_date: String,
+}
+
 #[derive(Deserialize, Serialize, Debug, Clone)]
 /// A playlist folder, not related to Spotify API yet
 pub struct PlaylistFolder {
@@ -245,6 +254,10 @@ impl Context {
             }
             Context::Artist { ref artist, .. } => artist.name.to_string(),
             Context::Tracks { desc, tracks } => format!("{} | {} songs", desc, tracks.len()),
+            Context::Show {
+                ref show,
+                ref episodes,
+            } => format!("{} | {} episodes", show.name, episodes.len()),
         }
     }
 }
@@ -256,6 +269,7 @@ impl ContextId {
             Self::Artist(id) => id.uri(),
             Self::Playlist(id) => id.uri(),
             Self::Tracks(id) => id.uri.clone(),
+            Self::Show(id) => id.uri(),
         }
     }
 }
@@ -309,11 +323,11 @@ impl Track {
     pub fn try_from_simplified_track(track: rspotify_model::SimplifiedTrack) -> Option<Self> {
         if track.is_playable.unwrap_or(true) {
             let id = match track.linked_from {
-                Some(d) => d.id,
-                None => track.id,
+                Some(d) => d.id?,
+                None => track.id?,
             };
             Some(Self {
-                id: id?,
+                id,
                 name: track.name,
                 artists: from_simplified_artists_to_artists(track.artists),
                 album: None,
@@ -330,11 +344,11 @@ impl Track {
     pub fn try_from_full_track(track: rspotify_model::FullTrack) -> Option<Self> {
         if track.is_playable.unwrap_or(true) {
             let id = match track.linked_from {
-                Some(d) => d.id,
-                None => track.id,
+                Some(d) => d.id?,
+                None => track.id?,
             };
             Some(Self {
-                id: id?,
+                id,
                 name: track.name,
                 artists: from_simplified_artists_to_artists(track.artists),
                 album: Album::try_from_simplified_album(track.album),
@@ -501,6 +515,66 @@ impl std::fmt::Display for Playlist {
     }
 }
 
+impl From<rspotify_model::SimplifiedShow> for Show {
+    fn from(show: rspotify_model::SimplifiedShow) -> Self {
+        Self {
+            id: show.id,
+            name: show.name,
+        }
+    }
+}
+
+impl From<rspotify_model::FullShow> for Show {
+    fn from(show: rspotify_model::FullShow) -> Self {
+        Self {
+            id: show.id,
+            name: show.name,
+        }
+    }
+}
+
+impl std::fmt::Display for Show {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.name)
+    }
+}
+
+impl From<rspotify_model::SimplifiedEpisode> for Episode {
+    fn from(episode: rspotify_model::SimplifiedEpisode) -> Self {
+        Self {
+            id: episode.id,
+            name: episode.name,
+            description: episode.description,
+            duration: episode.duration.to_std().expect("valid chrono duration"),
+            show: None,
+            release_date: episode.release_date,
+        }
+    }
+}
+
+impl From<rspotify_model::FullEpisode> for Episode {
+    fn from(episode: rspotify_model::FullEpisode) -> Self {
+        Self {
+            id: episode.id,
+            name: episode.name,
+            description: episode.description,
+            duration: episode.duration.to_std().expect("valid chrono duration"),
+            show: Some(episode.show.into()),
+            release_date: episode.release_date,
+        }
+    }
+}
+
+impl std::fmt::Display for Episode {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if let Some(s) = &self.show {
+            write!(f, "{} • {}", self.name, s.name)
+        } else {
+            write!(f, "{}", self.name)
+        }
+    }
+}
+
 impl std::fmt::Display for PlaylistFolder {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}/", self.name)
diff --git a/spotify_player/src/state/player.rs b/spotify_player/src/state/player.rs
index 2d440dc1..cc7f840d 100644
--- a/spotify_player/src/state/player.rs
+++ b/spotify_player/src/state/player.rs
@@ -1,5 +1,5 @@
 use super::model::{
-    rspotify_model, AlbumId, ArtistId, ContextId, Device, PlaybackMetadata, PlaylistId,
+    rspotify_model, AlbumId, ArtistId, ContextId, Device, PlaybackMetadata, PlaylistId, ShowId,
 };
 
 /// Player state
@@ -48,14 +48,8 @@ impl PlayerState {
         Some(playback)
     }
 
-    pub fn current_playing_track(&self) -> Option<&rspotify_model::FullTrack> {
-        match self.playback {
-            None => None,
-            Some(ref playback) => match playback.item {
-                Some(rspotify::model::PlayableItem::Track(ref track)) => Some(track),
-                _ => None,
-            },
-        }
+    pub fn currently_playing(&self) -> Option<&rspotify_model::PlayableItem> {
+        self.playback.as_ref().and_then(|p| p.item.as_ref())
     }
 
     pub fn playback_progress(&self) -> Option<chrono::Duration> {
@@ -91,6 +85,9 @@ impl PlayerState {
                         rspotify_model::Type::Artist => Some(ContextId::Artist(
                             ArtistId::from_uri(&uri).ok()?.into_static(),
                         )),
+                        rspotify_model::Type::Show => {
+                            Some(ContextId::Show(ShowId::from_uri(&uri).ok()?.into_static()))
+                        }
                         _ => None,
                     }
                 }
diff --git a/spotify_player/src/state/ui/page.rs b/spotify_player/src/state/ui/page.rs
index 31fb5b25..0ea16f17 100644
--- a/spotify_player/src/state/ui/page.rs
+++ b/spotify_player/src/state/ui/page.rs
@@ -63,6 +63,8 @@ pub struct SearchPageUIState {
     pub album_list: ListState,
     pub artist_list: ListState,
     pub playlist_list: ListState,
+    pub show_list: ListState,
+    pub episode_list: ListState,
     pub focus: SearchFocusState,
 }
 
@@ -89,19 +91,9 @@ pub enum ContextPageUIState {
     Tracks {
         track_table: TableState,
     },
-}
-
-impl ContextPageUIState {
-    pub fn track_table_mut(&mut self) -> &mut TableState {
-        match self {
-            Self::Playlist { track_table }
-            | Self::Album { track_table }
-            | Self::Tracks { track_table } => track_table,
-            Self::Artist {
-                top_track_table, ..
-            } => top_track_table,
-        }
-    }
+    Show {
+        episode_table: TableState,
+    },
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -125,6 +117,8 @@ pub enum SearchFocusState {
     Albums,
     Artists,
     Playlists,
+    Shows,
+    Episodes,
 }
 
 #[derive(Clone, Debug)]
@@ -198,6 +192,8 @@ impl PageState {
                         album_list,
                         artist_list,
                         playlist_list,
+                        show_list,
+                        episode_list,
                         focus,
                     },
                 ..
@@ -207,6 +203,8 @@ impl PageState {
                 SearchFocusState::Albums => Some(MutableWindowState::List(album_list)),
                 SearchFocusState::Artists => Some(MutableWindowState::List(artist_list)),
                 SearchFocusState::Playlists => Some(MutableWindowState::List(playlist_list)),
+                SearchFocusState::Shows => Some(MutableWindowState::List(show_list)),
+                SearchFocusState::Episodes => Some(MutableWindowState::List(episode_list)),
             },
             Self::Context { state, .. } => state.as_mut().map(|state| match state {
                 ContextPageUIState::Tracks { track_table }
@@ -226,6 +224,9 @@ impl PageState {
                         MutableWindowState::List(related_artist_list)
                     }
                 },
+                ContextPageUIState::Show { episode_table } => {
+                    MutableWindowState::Table(episode_table)
+                }
             }),
             Self::Browse { state } => match state {
                 BrowsePageUIState::CategoryList { state } => Some(MutableWindowState::List(state)),
@@ -261,6 +262,8 @@ impl SearchPageUIState {
             album_list: ListState::default(),
             artist_list: ListState::default(),
             playlist_list: ListState::default(),
+            show_list: ListState::default(),
+            episode_list: ListState::default(),
             focus: SearchFocusState::Input,
         }
     }
@@ -275,6 +278,7 @@ impl ContextPageType {
                 ContextId::Album(_) => String::from("Album"),
                 ContextId::Artist(_) => String::from("Artist"),
                 ContextId::Tracks(id) => id.kind.clone(),
+                ContextId::Show(_) => String::from("Show"),
             },
         }
     }
@@ -307,9 +311,15 @@ impl ContextPageUIState {
             track_table: TableState::default(),
         }
     }
+
+    pub fn new_show() -> Self {
+        Self::Show {
+            episode_table: TableState::default(),
+        }
+    }
 }
 
-impl<'a> MutableWindowState<'a> {
+impl MutableWindowState<'_> {
     pub fn select(&mut self, id: usize) {
         match self {
             Self::List(state) => state.select(Some(id)),
@@ -424,5 +434,7 @@ impl_focusable!(
     [Tracks, Albums],
     [Albums, Artists],
     [Artists, Playlists],
-    [Playlists, Input]
+    [Playlists, Shows],
+    [Shows, Episodes],
+    [Episodes, Input]
 );
diff --git a/spotify_player/src/state/ui/popup.rs b/spotify_player/src/state/ui/popup.rs
index d09de66f..ecdc5a24 100644
--- a/spotify_player/src/state/ui/popup.rs
+++ b/spotify_player/src/state/ui/popup.rs
@@ -1,6 +1,6 @@
 use crate::{
     command,
-    state::model::{Album, Artist, Playlist, Track, TrackId},
+    state::model::{Album, Artist, Episode, EpisodeId, Playlist, Show, Track, TrackId},
     ui::single_line_input::LineInput,
 };
 use tui::widgets::ListState;
@@ -36,6 +36,8 @@ pub enum ActionListItem {
     Artist(Artist, Vec<command::Action>),
     Album(Album, Vec<command::Action>),
     Playlist(Playlist, Vec<command::Action>),
+    Show(Show, Vec<command::Action>),
+    Episode(Episode, Vec<command::Action>),
 }
 
 /// An action on an item in a playlist popup list
@@ -48,6 +50,10 @@ pub enum PlaylistPopupAction {
         folder_id: usize,
         track_id: TrackId<'static>,
     },
+    AddEpisode {
+        folder_id: usize,
+        episode_id: EpisodeId<'static>,
+    },
 }
 
 /// An action on an item in an artist popup list
@@ -109,7 +115,9 @@ impl ActionListItem {
             ActionListItem::Track(.., actions)
             | ActionListItem::Artist(.., actions)
             | ActionListItem::Album(.., actions)
-            | ActionListItem::Playlist(.., actions) => actions.len(),
+            | ActionListItem::Playlist(.., actions)
+            | ActionListItem::Show(.., actions)
+            | ActionListItem::Episode(.., actions) => actions.len(),
         }
     }
 
@@ -119,21 +127,19 @@ impl ActionListItem {
             ActionListItem::Artist(artist, ..) => &artist.name,
             ActionListItem::Album(album, ..) => &album.name,
             ActionListItem::Playlist(playlist, ..) => &playlist.name,
+            ActionListItem::Show(show, ..) => &show.name,
+            ActionListItem::Episode(episode, ..) => &episode.name,
         }
     }
 
     pub fn actions_desc(&self) -> Vec<String> {
         match self {
-            ActionListItem::Track(.., actions) => {
-                actions.iter().map(|a| format!("{a:?}")).collect::<Vec<_>>()
-            }
-            ActionListItem::Artist(.., actions) => {
-                actions.iter().map(|a| format!("{a:?}")).collect::<Vec<_>>()
-            }
-            ActionListItem::Album(.., actions) => {
-                actions.iter().map(|a| format!("{a:?}")).collect::<Vec<_>>()
-            }
-            ActionListItem::Playlist(.., actions) => {
+            ActionListItem::Track(.., actions)
+            | ActionListItem::Artist(.., actions)
+            | ActionListItem::Album(.., actions)
+            | ActionListItem::Playlist(.., actions)
+            | ActionListItem::Show(.., actions)
+            | ActionListItem::Episode(.., actions) => {
                 actions.iter().map(|a| format!("{a:?}")).collect::<Vec<_>>()
             }
         }
diff --git a/spotify_player/src/streaming.rs b/spotify_player/src/streaming.rs
index f4a84b39..48b79ed0 100644
--- a/spotify_player/src/streaming.rs
+++ b/spotify_player/src/streaming.rs
@@ -11,7 +11,7 @@ use librespot_playback::{
     mixer::{self, Mixer},
     player,
 };
-use rspotify::model::TrackId;
+use rspotify::model::{EpisodeId, Id, PlayableId, TrackId};
 use serde::Serialize;
 use std::sync::Arc;
 
@@ -40,18 +40,18 @@ For more information, visit https://github.com/aome510/spotify-player?tab=readme
 #[derive(Debug, Serialize)]
 enum PlayerEvent {
     Changed {
-        track_id: TrackId<'static>,
+        playable_id: PlayableId<'static>,
     },
     Playing {
-        track_id: TrackId<'static>,
+        playable_id: PlayableId<'static>,
         position_ms: u32,
     },
     Paused {
-        track_id: TrackId<'static>,
+        playable_id: PlayableId<'static>,
         position_ms: u32,
     },
     EndOfTrack {
-        track_id: TrackId<'static>,
+        playable_id: PlayableId<'static>,
     },
 }
 
@@ -59,47 +59,58 @@ impl PlayerEvent {
     /// gets the event's arguments
     pub fn args(&self) -> Vec<String> {
         match self {
-            PlayerEvent::Changed { track_id } => vec!["Changed".to_string(), track_id.to_string()],
+            PlayerEvent::Changed { playable_id } => {
+                vec!["Changed".to_string(), playable_id.uri()]
+            }
             PlayerEvent::Playing {
-                track_id,
+                playable_id,
                 position_ms,
             } => vec![
                 "Playing".to_string(),
-                track_id.to_string(),
+                playable_id.uri(),
                 position_ms.to_string(),
             ],
             PlayerEvent::Paused {
-                track_id,
+                playable_id,
                 position_ms,
             } => vec![
                 "Paused".to_string(),
-                track_id.to_string(),
+                playable_id.uri(),
                 position_ms.to_string(),
             ],
-            PlayerEvent::EndOfTrack { track_id } => {
-                vec!["EndOfTrack".to_string(), track_id.to_string()]
+            PlayerEvent::EndOfTrack { playable_id } => {
+                vec!["EndOfTrack".to_string(), playable_id.uri()]
             }
         }
     }
 }
 
-fn spotify_id_to_track_id(id: spotify_id::SpotifyId) -> anyhow::Result<TrackId<'static>> {
-    let uri = id.to_uri()?;
-    Ok(TrackId::from_uri(&uri)?.into_static())
+fn spotify_id_to_playable_id(id: spotify_id::SpotifyId) -> anyhow::Result<PlayableId<'static>> {
+    match id.item_type {
+        spotify_id::SpotifyItemType::Track => {
+            let uri = id.to_uri()?;
+            Ok(TrackId::from_uri(&uri)?.into_static().into())
+        }
+        spotify_id::SpotifyItemType::Episode => {
+            let uri = id.to_uri()?;
+            Ok(EpisodeId::from_uri(&uri)?.into_static().into())
+        }
+        _ => anyhow::bail!("unexpected spotify_id {:?}", id),
+    }
 }
 
 impl PlayerEvent {
     pub fn from_librespot_player_event(e: player::PlayerEvent) -> anyhow::Result<Option<Self>> {
         Ok(match e {
             player::PlayerEvent::TrackChanged { audio_item } => Some(PlayerEvent::Changed {
-                track_id: spotify_id_to_track_id(audio_item.track_id)?,
+                playable_id: spotify_id_to_playable_id(audio_item.track_id)?,
             }),
             player::PlayerEvent::Playing {
                 track_id,
                 position_ms,
                 ..
             } => Some(PlayerEvent::Playing {
-                track_id: spotify_id_to_track_id(track_id)?,
+                playable_id: spotify_id_to_playable_id(track_id)?,
                 position_ms,
             }),
             player::PlayerEvent::Paused {
@@ -107,11 +118,11 @@ impl PlayerEvent {
                 position_ms,
                 ..
             } => Some(PlayerEvent::Paused {
-                track_id: spotify_id_to_track_id(track_id)?,
+                playable_id: spotify_id_to_playable_id(track_id)?,
                 position_ms,
             }),
             player::PlayerEvent::EndOfTrack { track_id, .. } => Some(PlayerEvent::EndOfTrack {
-                track_id: spotify_id_to_track_id(track_id)?,
+                playable_id: spotify_id_to_playable_id(track_id)?,
             }),
             _ => None,
         })
diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs
index 42418ccf..ec51ef82 100644
--- a/spotify_player/src/ui/page.rs
+++ b/spotify_player/src/ui/page.rs
@@ -1,6 +1,9 @@
-use std::collections::{btree_map::Entry, BTreeMap};
+use std::{
+    collections::{btree_map::Entry, BTreeMap},
+    fmt::Display,
+};
 
-use crate::utils::format_duration;
+use crate::{state::Episode, utils::format_duration};
 
 use super::{
     config, rspotify_model, utils, utils::construct_and_render_block, Album, Artist,
@@ -52,21 +55,22 @@ pub fn render_search_page(
     let search_input_rect = chunks[0];
     let rect = chunks[1];
 
-    // track/album/artist/playlist search results layout
+    // track/album/artist/playlist/show/episode search results layout
     let chunks = match ui.orientation {
+        // 1x6
         Orientation::Vertical => {
             let constraints = if focus_state == SearchFocusState::Input {
-                [Constraint::Ratio(1, 4); 4]
+                [Constraint::Ratio(1, 6); 6]
             } else {
-                let mut constraints = [Constraint::Percentage(20); 4];
-                constraints[focus_state as usize - 1] = Constraint::Percentage(40);
+                let mut constraints = [Constraint::Percentage(15); 6];
+                constraints[focus_state as usize - 1] = Constraint::Percentage(25);
                 constraints
             };
 
             Layout::vertical(constraints).split(rect)
         }
-        // 2x2 table
-        Orientation::Horizontal => Layout::vertical([Constraint::Ratio(1, 2); 2])
+        // 2x3
+        Orientation::Horizontal => Layout::vertical([Constraint::Ratio(1, 3); 3])
             .split(rect)
             .iter()
             .flat_map(|rect| {
@@ -103,21 +107,24 @@ pub fn render_search_page(
     );
     let playlist_rect =
         construct_and_render_block("Playlists", &ui.theme, Borders::TOP, frame, chunks[3]);
+    let show_rect = construct_and_render_block(
+        "Shows",
+        &ui.theme,
+        Borders::TOP | Borders::RIGHT,
+        frame,
+        chunks[4],
+    );
+    let episode_rect =
+        construct_and_render_block("Episodes", &ui.theme, Borders::TOP, frame, chunks[5]);
+
+    fn search_items<T: Display>(items: &[T]) -> Vec<(String, bool)> {
+        items.iter().map(|i| (i.to_string(), false)).collect()
+    }
 
     // 3. Construct the page's widgets
     let (track_list, n_tracks) = {
         let track_items = search_results
-            .map(|s| {
-                s.tracks
-                    .iter()
-                    .map(|a| {
-                        (
-                            format!("{} • {}", a.display_name(), a.artists_info()),
-                            false,
-                        )
-                    })
-                    .collect::<Vec<_>>()
-            })
+            .map(|s| search_items(&s.tracks))
             .unwrap_or_default();
 
         let is_active = is_active && focus_state == SearchFocusState::Tracks;
@@ -127,12 +134,7 @@ pub fn render_search_page(
 
     let (album_list, n_albums) = {
         let album_items = search_results
-            .map(|s| {
-                s.albums
-                    .iter()
-                    .map(|a| (a.to_string(), false))
-                    .collect::<Vec<_>>()
-            })
+            .map(|s| search_items(&s.albums))
             .unwrap_or_default();
 
         let is_active = is_active && focus_state == SearchFocusState::Albums;
@@ -142,12 +144,7 @@ pub fn render_search_page(
 
     let (artist_list, n_artists) = {
         let artist_items = search_results
-            .map(|s| {
-                s.artists
-                    .iter()
-                    .map(|a| (a.to_string(), false))
-                    .collect::<Vec<_>>()
-            })
+            .map(|s| search_items(&s.artists))
             .unwrap_or_default();
 
         let is_active = is_active && focus_state == SearchFocusState::Artists;
@@ -157,12 +154,7 @@ pub fn render_search_page(
 
     let (playlist_list, n_playlists) = {
         let playlist_items = search_results
-            .map(|s| {
-                s.playlists
-                    .iter()
-                    .map(|a| (a.to_string(), false))
-                    .collect::<Vec<_>>()
-            })
+            .map(|s| search_items(&s.playlists))
             .unwrap_or_default();
 
         let is_active = is_active && focus_state == SearchFocusState::Playlists;
@@ -170,6 +162,25 @@ pub fn render_search_page(
         utils::construct_list_widget(&ui.theme, playlist_items, is_active)
     };
 
+    let (show_list, n_shows) = {
+        let show_items = search_results
+            .map(|s| search_items(&s.shows))
+            .unwrap_or_default();
+        let is_active = is_active && focus_state == SearchFocusState::Shows;
+
+        utils::construct_list_widget(&ui.theme, show_items, is_active)
+    };
+
+    let (episode_list, n_episodes) = {
+        let episode_items = search_results
+            .map(|s| search_items(&s.episodes))
+            .unwrap_or_default();
+
+        let is_active = is_active && focus_state == SearchFocusState::Episodes;
+
+        utils::construct_list_widget(&ui.theme, episode_items, is_active)
+    };
+
     // 4. Render the page's widgets
     // Render the query input box
     frame.render_widget(
@@ -213,6 +224,20 @@ pub fn render_search_page(
         n_playlists,
         &mut page_state.playlist_list,
     );
+    utils::render_list_window(
+        frame,
+        show_list,
+        show_rect,
+        n_shows,
+        &mut page_state.show_list,
+    );
+    utils::render_list_window(
+        frame,
+        episode_list,
+        episode_rect,
+        n_episodes,
+        &mut page_state.episode_list,
+    );
 }
 
 pub fn render_context_page(
@@ -316,6 +341,16 @@ pub fn render_context_page(
                         &data,
                     );
                 }
+                Context::Show { episodes, .. } => {
+                    render_episode_table(
+                        frame,
+                        rect,
+                        is_active,
+                        state,
+                        ui.search_filtered_items(episodes),
+                        ui,
+                    );
+                }
             }
         }
         None => {
@@ -657,7 +692,7 @@ pub fn render_queue_page(
                 .map(|a| a.name.as_str())
                 .collect::<Vec<_>>()
                 .join(", "),
-            PlayableItem::Episode(FullEpisode { .. }) => String::new(),
+            PlayableItem::Episode(FullEpisode { ref show, .. }) => show.publisher.clone(),
         }
     }
     fn get_playable_duration(item: &PlayableItem) -> String {
@@ -939,7 +974,97 @@ fn render_track_table(
         state: Some(state), ..
     } = ui.current_page_mut()
     {
-        let track_table_state = state.track_table_mut();
-        utils::render_table_window(frame, track_table, rect, n_tracks, track_table_state);
+        let playable_table_state = match state {
+            ContextPageUIState::Artist {
+                top_track_table, ..
+            } => top_track_table,
+            ContextPageUIState::Playlist { track_table }
+            | ContextPageUIState::Album { track_table }
+            | ContextPageUIState::Tracks { track_table } => track_table,
+            ContextPageUIState::Show { .. } => {
+                unreachable!("show's episode table should be handled by render_episode_table")
+            }
+        };
+        utils::render_table_window(frame, track_table, rect, n_tracks, playable_table_state);
+    }
+}
+
+fn render_episode_table(
+    frame: &mut Frame,
+    rect: Rect,
+    is_active: bool,
+    state: &SharedState,
+    episodes: Vec<&Episode>,
+    ui: &mut UIStateGuard,
+) {
+    let configs = config::get_config();
+    // get the current playing episode's URI to decorate such episode (if exists) in the episode table
+    let mut playing_episode_uri = String::new();
+    let mut playing_id = "";
+    if let Some(ref playback) = state.player.read().playback {
+        if let Some(rspotify_model::PlayableItem::Episode(ref episode)) = playback.item {
+            playing_episode_uri = episode.id.uri();
+
+            playing_id = if playback.is_playing {
+                &configs.app_config.play_icon
+            } else {
+                &configs.app_config.pause_icon
+            };
+        }
+    }
+
+    let n_episodes = episodes.len();
+    let rows = episodes
+        .into_iter()
+        .enumerate()
+        .map(|(id, e)| {
+            let (id, style) = if playing_episode_uri == e.id.uri() {
+                (playing_id.to_string(), ui.theme.current_playing())
+            } else {
+                ((id + 1).to_string(), Style::default())
+            };
+            Row::new(vec![
+                Cell::from(id),
+                Cell::from(e.name.clone()),
+                Cell::from(e.release_date.clone()),
+                Cell::from(format!(
+                    "{}:{:02}",
+                    e.duration.as_secs() / 60,
+                    e.duration.as_secs() % 60,
+                )),
+            ])
+            .style(style)
+        })
+        .collect::<Vec<_>>();
+    let episode_table = Table::new(
+        rows,
+        [
+            Constraint::Length(4),
+            Constraint::Fill(4),
+            Constraint::Fill(3),
+            Constraint::Fill(1),
+        ],
+    )
+    .header(
+        Row::new(vec![
+            Cell::from("#"),
+            Cell::from("Title"),
+            Cell::from("Date"),
+            Cell::from("Duration"),
+        ])
+        .style(ui.theme.table_header()),
+    )
+    .column_spacing(2)
+    .row_highlight_style(ui.theme.selection(is_active));
+
+    if let PageState::Context {
+        state: Some(state), ..
+    } = ui.current_page_mut()
+    {
+        let playable_table_state = match state {
+            ContextPageUIState::Show { episode_table } => episode_table,
+            s => unreachable!("unexpected state: {s:?}"),
+        };
+        utils::render_table_window(frame, episode_table, rect, n_episodes, playable_table_state);
     }
 }
diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs
index 935f1a96..4931eb6b 100644
--- a/spotify_player/src/ui/playback.rs
+++ b/spotify_player/src/ui/playback.rs
@@ -25,7 +25,7 @@ pub fn render_playback_window(
 
     let player = state.player.read();
     if let Some(ref playback) = player.playback {
-        if let Some(rspotify::model::PlayableItem::Track(ref track)) = playback.item {
+        if let Some(item) = &playback.item {
             let (metadata_rect, progress_bar_rect) = {
                 // allocate the progress bar rect
                 let (rect, progress_bar_rect) = {
@@ -57,7 +57,14 @@ pub fn render_playback_window(
                             (hor_chunks[1], ver_chunks[0])
                         };
 
-                        let url = crate::utils::get_track_album_image_url(track).map(String::from);
+                        let url = match item {
+                            rspotify_model::PlayableItem::Track(track) => {
+                                crate::utils::get_track_album_image_url(track).map(String::from)
+                            }
+                            rspotify_model::PlayableItem::Episode(episode) => {
+                                crate::utils::get_episode_show_image_url(episode).map(String::from)
+                            }
+                        };
                         if let Some(url) = url {
                             let needs_clear = if ui.last_cover_image_render_info.url != url
                                 || ui.last_cover_image_render_info.render_area != cover_img_rect
@@ -115,16 +122,21 @@ pub fn render_playback_window(
             };
 
             if let Some(ref playback) = player.buffered_playback {
-                let playback_text = construct_playback_text(ui, track, playback);
+                let playback_text = construct_playback_text(ui, item, playback);
                 let playback_desc = Paragraph::new(playback_text);
                 frame.render_widget(playback_desc, metadata_rect);
             }
 
+            let duration = match item {
+                rspotify_model::PlayableItem::Track(track) => track.duration,
+                rspotify_model::PlayableItem::Episode(episode) => episode.duration,
+            };
+
             let progress = std::cmp::min(
                 player.playback_progress().expect("non-empty playback"),
-                track.duration,
+                duration,
             );
-            render_playback_progress_bar(frame, ui, progress, track, progress_bar_rect);
+            render_playback_progress_bar(frame, ui, progress, duration, progress_bar_rect);
         }
     } else {
         // Previously rendered image can result in a weird rendering text,
@@ -166,7 +178,7 @@ fn clear_area(frame: &mut Frame, rect: Rect) {
 
 fn construct_playback_text(
     ui: &UIStateGuard,
-    track: &rspotify_model::FullTrack,
+    playable: &rspotify_model::PlayableItem,
     playback: &PlaybackMetadata,
 ) -> Text<'static> {
     // Construct a "styled" text (`playback_text`) from playback's data
@@ -206,19 +218,41 @@ fn construct_playback_text(
                 .to_owned(),
                 ui.theme.playback_status(),
             ),
-            "{track}" => (
-                if track.explicit {
-                    format!("{} (E)", track.name)
-                } else {
-                    track.name.clone()
-                },
-                ui.theme.playback_track(),
-            ),
-            "{artists}" => (
-                crate::utils::map_join(&track.artists, |a| &a.name, ", "),
-                ui.theme.playback_artists(),
-            ),
-            "{album}" => (track.album.name.clone(), ui.theme.playback_album()),
+            "{track}" => match playable {
+                rspotify_model::PlayableItem::Track(track) => (
+                    if track.explicit {
+                        format!("{} (E)", track.name)
+                    } else {
+                        track.name.clone()
+                    },
+                    ui.theme.playback_track(),
+                ),
+                rspotify_model::PlayableItem::Episode(episode) => (
+                    if episode.explicit {
+                        format!("{} (E)", episode.name)
+                    } else {
+                        episode.name.clone()
+                    },
+                    ui.theme.playback_track(),
+                ),
+            },
+            "{artists}" => match playable {
+                rspotify_model::PlayableItem::Track(track) => (
+                    crate::utils::map_join(&track.artists, |a| &a.name, ", "),
+                    ui.theme.playback_artists(),
+                ),
+                rspotify_model::PlayableItem::Episode(episode) => {
+                    (episode.show.publisher.clone(), ui.theme.playback_artists())
+                }
+            },
+            "{album}" => match playable {
+                rspotify_model::PlayableItem::Track(track) => {
+                    (track.album.name.clone(), ui.theme.playback_album())
+                }
+                rspotify_model::PlayableItem::Episode(episode) => {
+                    (episode.show.name.clone(), ui.theme.playback_album())
+                }
+            },
             "{metadata}" => (
                 format!(
                     "repeat: {} | shuffle: {} | volume: {} | device: {}",
@@ -228,9 +262,10 @@ fn construct_playback_text(
                         <&'static str>::from(playback.repeat_state)
                     },
                     playback.shuffle_state,
-                    match playback.mute_state {
-                        Some(volume) => format!("{volume}% (muted)"),
-                        None => format!("{}%", playback.volume.unwrap_or_default()),
+                    if let Some(volume) = playback.mute_state {
+                        format!("{volume}% (muted)")
+                    } else {
+                        format!("{}%", playback.volume.unwrap_or_default())
                     },
                     playback.device_name,
                 ),
@@ -255,13 +290,12 @@ fn render_playback_progress_bar(
     frame: &mut Frame,
     ui: &mut UIStateGuard,
     progress: chrono::Duration,
-    track: &rspotify_model::FullTrack,
+    duration: chrono::Duration,
     rect: Rect,
 ) {
     // Negative numbers can sometimes appear from progress.num_seconds() so this stops
     // them coming through into the ratios
-    let ratio =
-        (progress.num_seconds() as f64 / track.duration.num_seconds() as f64).clamp(0.0, 1.0);
+    let ratio = (progress.num_seconds() as f64 / duration.num_seconds() as f64).clamp(0.0, 1.0);
 
     match config::get_config().app_config.progress_bar_type {
         config::ProgressBarType::Line => frame.render_widget(
@@ -273,7 +307,7 @@ fn render_playback_progress_bar(
                     format!(
                         "{}/{}",
                         crate::utils::format_duration(&progress),
-                        crate::utils::format_duration(&track.duration),
+                        crate::utils::format_duration(&duration),
                     ),
                     Style::default().add_modifier(Modifier::BOLD),
                 )),
@@ -287,7 +321,7 @@ fn render_playback_progress_bar(
                     format!(
                         "{}/{}",
                         crate::utils::format_duration(&progress),
-                        crate::utils::format_duration(&track.duration),
+                        crate::utils::format_duration(&duration),
                     ),
                     Style::default().add_modifier(Modifier::BOLD),
                 )),
diff --git a/spotify_player/src/ui/popup.rs b/spotify_player/src/ui/popup.rs
index 7d41de4e..d8edec33 100644
--- a/spotify_player/src/ui/popup.rs
+++ b/spotify_player/src/ui/popup.rs
@@ -113,7 +113,8 @@ pub fn render_popup(
                     PlaylistPopupAction::Browse { folder_id } => {
                         data.user_data.folder_playlists_items(*folder_id)
                     }
-                    PlaylistPopupAction::AddTrack { folder_id, .. } => {
+                    PlaylistPopupAction::AddTrack { folder_id, .. }
+                    | PlaylistPopupAction::AddEpisode { folder_id, .. } => {
                         data.user_data.modifiable_playlist_items(Some(*folder_id))
                     }
                 };
diff --git a/spotify_player/src/utils.rs b/spotify_player/src/utils.rs
index 894d445d..a8d905d1 100644
--- a/spotify_player/src/utils.rs
+++ b/spotify_player/src/utils.rs
@@ -28,6 +28,15 @@ pub fn get_track_album_image_url(track: &rspotify::model::FullTrack) -> Option<&
     }
 }
 
+#[allow(dead_code)]
+pub fn get_episode_show_image_url(episode: &rspotify::model::FullEpisode) -> Option<&str> {
+    if episode.show.images.is_empty() {
+        None
+    } else {
+        Some(&episode.show.images[0].url)
+    }
+}
+
 pub fn parse_uri(uri: &str) -> Cow<str> {
     let parts = uri.split(':').collect::<Vec<_>>();
     // The below URI probably has a format of `spotify:user:{user_id}:{type}:{id}`,


Test patch: