## ----eval = FALSE-------------------------------------------------------------
# # Example Shiny app using shinyOAuth to connect to Spotify API
# #
# # This app demonstrates logging into Spotify with shinyOAuth and fetching
# # various user statistics via the Spotify Web API. We then build a simple
# # dashboard to display this information
# #
# # Requirements:
# # - Create a Spotify OAuth 2.0 application at https://developer.spotify.com
# # - Add a redirect URI that matches redirect_uri below (default: http://127.0.0.1:8100)
# # - Set environment variables `SPOTIFY_OAUTH_CLIENT_ID` and `SPOTIFY_OAUTH_CLIENT_SECRET`
# 
# # Load packages & configure OAuth 2.0 client for Spotify -----------------------
# 
# library(shiny)
# library(shinyOAuth)
# library(bslib)
# library(ggplot2)
# library(DT)
# 
# # Configure provider and client for Spotify
# 
# provider <- oauth_provider_spotify()
# 
# client <- oauth_client(
#   provider = provider,
#   client_id = Sys.getenv("SPOTIFY_OAUTH_CLIENT_ID"),
#   client_secret = Sys.getenv("SPOTIFY_OAUTH_CLIENT_SECRET"),
#   redirect_uri = "http://127.0.0.1:8100",
#   scopes = c(
#     "user-read-email",
#     "user-read-private",
#     "user-top-read",
#     "user-read-recently-played",
#     "user-read-playback-state",
#     "user-read-currently-playing"
#   )
# )
# 
# 
# # Spotify API helpers ----------------------------------------------------------
# 
# # Small helpers to call Spotify API with the user's access token
# # We define a few specialized functions for common endpoints
# 
# spotify_get <- function(token, path, query = list()) {
#   url <- paste0("https://api.spotify.com", path)
# 
#   req <- client_bearer_req(token, url, query = query)
#   resp <- httr2::req_perform(req)
# 
#   if (httr2::resp_is_error(resp)) {
#     msg <- sprintf("Spotify API error: HTTP %s", httr2::resp_status(resp))
#     stop(msg, call. = FALSE)
#   }
# 
#   httr2::resp_body_json(resp, simplifyVector = TRUE)
# }
# 
# # Specialized helper for endpoints that may return 204 (e.g., currently-playing)
# spotify_get_maybe_empty <- function(token, path, query = list()) {
#   url <- paste0("https://api.spotify.com", path)
# 
#   req <- client_bearer_req(token, url, query = query)
#   resp <- httr2::req_perform(req)
# 
#   status <- httr2::resp_status(resp)
#   if (status == 204L) {
#     return(NULL)
#   }
# 
#   if (httr2::resp_is_error(resp)) {
#     msg <- sprintf("Spotify API error: HTTP %s", status)
#     stop(msg, call. = FALSE)
#   }
# 
#   httr2::resp_body_json(resp, simplifyVector = TRUE)
# }
# 
# # Fetch top tracks and artists (short_term: last 4 weeks)
# get_top_tracks <- function(token, limit = 10, time_range = "short_term") {
#   out <- spotify_get(
#     token,
#     "/v1/me/top/tracks",
#     query = list(limit = limit, time_range = time_range)
#   )
# 
#   items <- out$items %||% list()
#   if (length(items) == 0) {
#     return(data.frame())
#   }
# 
#   df <- purrr::map(seq_along(items), function(i) {
#     item <- items[i, ]
#     data.frame(
#       name = item$name %||% NA_character_,
#       artist = paste(item$artists[[1]]$name, collapse = ", "),
#       album = item$album$name %||% NA_character_,
#       popularity = as.numeric(item$popularity) %||% NA_real_,
#       stringsAsFactors = FALSE
#     )
#   }) |>
#     dplyr::bind_rows()
# 
#   df
# }
# 
# # Fetch top artists
# get_top_artists <- function(token, limit = 10, time_range = "short_term") {
#   out <- spotify_get(
#     token,
#     "/v1/me/top/artists",
#     query = list(limit = limit, time_range = time_range)
#   )
# 
#   items <- out$items %||% list()
#   if (length(items) == 0) {
#     return(data.frame())
#   }
# 
#   df <- purrr::map(seq_along(items), function(i) {
#     item <- items[i, ]
#     data.frame(
#       name = item$name %||% NA_character_,
#       genres = paste(
#         as.character(item$genres |> purrr::flatten() %||% character()),
#         collapse = ", "
#       ),
#       popularity = as.numeric(item$popularity) %||% NA_real_,
#       followers = as.numeric(item$followers$total %||% NA_real_),
#       stringsAsFactors = FALSE
#     )
#   }) |>
#     dplyr::bind_rows()
# 
#   df
# }
# 
# # Get recently played tracks
# get_recently_played <- function(token, limit = 20) {
#   out <- spotify_get(
#     token,
#     "/v1/me/player/recently-played",
#     query = list(limit = limit)
#   )
# 
#   items <- out$items %||% list()
#   if (length(items) == 0) {
#     return(data.frame())
#   }
# 
#   df <- purrr::map(seq_along(items), function(i) {
#     item <- items[i, ]
#     data.frame(
#       played_at = as.POSIXct(item$played_at %||% NA_character_, tz = "UTC"),
#       track = item$track$name %||% NA_character_,
#       artist = paste(item$track$artists[[1]]$name, collapse = ", "),
#       album = item$track$album$name %||% NA_character_,
#       stringsAsFactors = FALSE
#     )
#   }) |>
#     dplyr::bind_rows()
# 
#   df
# }
# 
# # Currently playing (may be NULL if nothing is playing)
# get_currently_playing <- function(token) {
#   out <- spotify_get_maybe_empty(token, "/v1/me/player/currently-playing")
# 
#   if (is.null(out)) {
#     return(NULL)
#   }
# 
#   # Normalize essential fields with guards
#   item <- out$item
# 
#   if (is.null(item)) {
#     return(NULL)
#   }
# 
#   artists <- tryCatch(
#     {
#       if (!is.null(item$artists) && length(item$artists) > 0) {
#         paste(item$artists$name, collapse = ", ")
#       } else {
#         "—"
#       }
#     },
#     error = function(e) "—"
#   )
# 
#   art_url <- tryCatch(
#     {
#       item$album$images$url[[1]]
#     },
#     error = function(e) NULL
#   )
# 
#   list(
#     is_playing = isTRUE(out$is_playing),
#     progress_ms = as.numeric(out$progress_ms %||% NA_real_),
#     duration_ms = as.numeric(item$duration_ms %||% NA_real_),
#     track = item$name %||% "—",
#     artist = artists,
#     album = item$album$name %||% "—",
#     art = art_url
#   )
# }
# 
# # Helper to safely validate data frames returned from API calls
# safe_df <- function(x) {
#   if (inherits(x, "try-error")) {
#     return(NULL)
#   }
# 
#   if (is.null(x) || !is.data.frame(x) || nrow(x) == 0) {
#     return(NULL)
#   }
# 
#   x
# }
# 
# # Format milliseconds to m:ss
# format_ms <- function(ms) {
#   if (is.null(ms) || is.na(ms)) {
#     return("—")
#   }
# 
#   s <- round(as.numeric(ms) / 1000)
# 
#   sprintf("%d:%02d", s %/% 60, s %% 60)
# }
# 
# 
# # Shiny app --------------------------------------------------------------------
# 
# ## Theme & CSS -----------------------------------------------------------------
# 
# # Some basic Bootstrap theming
# spotify_theme <- bs_theme(
#   version = 5,
#   base_font = font_google("Inter"),
#   heading_font = font_google("Space Grotesk"),
#   bg = "#121212",
#   fg = "#F5F6F8",
#   primary = "#1DB954",
#   secondary = "#191414",
#   success = "#1ED760",
#   "navbar-bg" = "#0F0F0F",
#   "card-border-color" = "#1DB95433"
# )
# 
# # Add CSS
# spotify_theme <- bs_add_rules(
#   spotify_theme,
#   paste(
#     "body { background: radial-gradient(circle at top left, #1DB95411, #121212 55%); }",
#     ".navbar-dark { border-bottom: 1px solid #1DB95422; }",
#     ".card { background-color: #181818; border-radius: 18px; box-shadow: 0 18px 30px -24px rgba(0,0,0,0.7); transition: transform 0.2s, box-shadow 0.2s; }",
#     ".card:hover { box-shadow: 0 20px 35px -20px rgba(29, 185, 84, 0.3); }",
#     ".card-header { background-color: rgba(29, 185, 84, 0.08); border-bottom: 1px solid rgba(29, 185, 84, 0.2); font-weight: 600; }",
#     ".profile-avatar { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; box-shadow: 0 0 0 3px #1DB95455; transition: box-shadow 0.3s; }",
#     ".profile-avatar:hover { box-shadow: 0 0 0 4px #1DB954; }",
#     ".login-hero { min-height: 60vh; }",
#     ".login-card { background: linear-gradient(130deg, #1DB954 0%, #1AA34A 55%, #121212 100%); color: #0C0C0C; border: none; }",
#     ".login-card .btn { background-color: #121212; color: #F5F6F8; border: none; transition: all 0.3s; }",
#     ".login-card .btn:hover { background-color: #0f0f0f; color: #1DB954; transform: scale(1.05); }",
#     ".value-box { background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%); border: 1px solid #1DB95433; border-radius: 12px; transition: border-color 0.3s; padding: 0.6rem 0.9rem !important; }",
#     ".value-box:hover { border-color: #1DB95466; }",
#     ".value-box .value { font-size: 1.3rem; font-weight: 700; color: #1DB954; line-height: 1.2; }",
#     ".value-box .value-box-title, .value-box h6, .value-box .title { font-size: 0.85rem; letter-spacing: .02em; opacity: .95; }",
#     ".value-box p { margin-bottom: 0; font-size: 0.9rem; }",
#     ".value-box .showcase-icon { color: #1DB954; opacity: 0.7; }",
#     ".table { color: #F5F6F8; margin-bottom: 0; }",
#     ".table thead { color: #1DB954; font-weight: 600; border-bottom: 2px solid #1DB95444; }",
#     ".table tbody tr { transition: background-color 0.2s; }",
#     ".table tbody tr:hover { background-color: rgba(29, 185, 84, 0.15); }",
#     ".table td { vertical-align: middle; padding: 0.75rem; }",
#     ".table td:first-child { color: #1DB954; font-weight: 600; width: 40px; text-align: center; }",
#     ".control-card { background: rgba(16, 16, 16, 0.7); border: 1px solid #1DB95422; }",
#     ".badge { font-size: 0.85rem; padding: 0.4em 0.8em; }",
#     ".play-count-badge { background: linear-gradient(135deg, #1DB954 0%, #1AA34A 100%); color: #000; font-weight: 700; }",
#     ".navbar .navbar-nav { display: none !important; }",
#     sep = "\n"
#   )
# )
# 
# # Subtle readability and responsive polish overrides
# spotify_theme <- bs_add_rules(
#   spotify_theme,
#   paste(
#     "/* Ensure cards don't collapse too small on narrow screens */",
#     ".card { min-width: 300px; }",
#     "/* Avoid horizontal scroll within cards */",
#     ".card .card-body { overflow-x: hidden; }",
#     "/* Add gap between cards in layout_columns */",
#     ".bslib-grid { gap: 1rem !important; }",
#     "/* Ensure proper wrapping for cards - prevent cards from becoming too narrow */",
#     ".bslib-grid > div { min-width: 300px; flex: 1 1 300px; }",
#     "/* Prevent value box containers from collapsing */",
#     ".card-body .bslib-grid { display: flex; flex-wrap: wrap; }",
# 
#     "/* Softer login gradient and better contrast */",
#     ".login-card { background: linear-gradient(145deg, rgba(29,185,84,0.18) 0%, rgba(29,185,84,0.08) 38%, #1a1a1a 100%); color: #F5F6F8; border: 1px solid #1DB95422; overflow: hidden; }",
#     ".login-card .btn { background-color: #121212; color: #F5F6F8; border: 1px solid #1DB95444; transition: background-color 0.25s, color 0.25s, box-shadow 0.25s; }",
#     ".login-card .btn:hover { background-color: #0f0f0f; color: #1DB954; box-shadow: 0 8px 22px rgba(29,185,84,0.22); }",
#     ".login-card .btn:focus, .login-card .btn:focus-visible { outline: none; box-shadow: 0 0 0 0.2rem rgba(29,185,84,0.35); }",
# 
#     "/* Improve muted text contrast inside cards/value boxes */",
#     ".card .text-muted, .value-box .text-muted { color: #CFD3D8 !important; }",
# 
#     "/* DataTables dark theme tweaks */",
#     ".dataTables_wrapper .dataTables_length select, .dataTables_wrapper .dataTables_filter input { background-color: #0f0f0f; color: #F5F6F8; border: 1px solid #1DB95433; }",
#     ".dataTables_wrapper .dataTables_paginate .paginate_button { color: #F5F6F8 !important; border: 1px solid transparent; }",
#     ".dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button:hover { color: #1DB954 !important; background: #0f0f0f; border-color: #1DB95433; }",
#     ".dataTables_wrapper .dataTables_info { color: #E4E7EB; }",
# 
#     "/* Slightly more visible table header border for clarity */",
#     ".table thead { border-bottom: 2px solid #1DB95455; }",
# 
#     "/* Custom Spotify outline button (for Sign out) */",
#     ".btn-spotify-outline { color: #1DB954; border: 1px solid #1DB95499; background: transparent; }",
#     ".btn-spotify-outline:hover { color: #0b0b0b; background: #1DB954; border-color: #1DB954; }",
# 
#     "/* Plan badge for better readability */",
#     ".badge-plan { background: transparent; border: 1px solid #1DB95466; color: #F5F6F8; }",
# 
#     "/* Sidebar toggle visibility */",
#     ".layout-sidebar .collapse-toggle, .layout-sidebar .sidebar-toggle, .bslib-sidebar-layout .collapse-toggle { color: #F5F6F8; border: 1px solid #1DB95455; background: #0f0f0f; }",
#     ".layout-sidebar .collapse-toggle:hover, .layout-sidebar .sidebar-toggle:hover, .bslib-sidebar-layout .collapse-toggle:hover { border-color: #1DB954aa; color: #1DB954; }",
# 
#     "/* Value box compact sizing and min width with proper wrapping */",
#     ".value-box { min-width: 220px; margin-bottom: 0.75rem; flex: 1 1 220px; }",
#     ".value-box .showcase-top, .value-box .showcase-bottom, .value-box .showcase-area { gap: .5rem; }",
#     ".value-box .showcase-icon { font-size: 0.95rem; }",
#     "/* Prevent value box text overflow */",
#     ".value-box .value { word-break: break-word; font-size: 1.1rem !important; }",
#     ".value-box p { word-break: break-word; overflow-wrap: break-word; font-size: 0.85rem; }",
#     ".value-box .title, .value-box h6 { font-size: 0.8rem; }",
# 
#     "/* Now playing artwork sizing */",
#     ".now-playing-art { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; box-shadow: 0 8px 18px rgba(0,0,0,.35); }",
# 
#     sep = "\n"
#   )
# )
# 
# 
# ## UI --------------------------------------------------------------------------
# 
# ui <- bslib::page_fluid(
#   title = tags$span(
#     class = "d-flex align-items-center gap-2",
#     icon("headphones"),
#     span(class = "fw-semibold", "Spotify Listening Studio")
#   ),
#   theme = spotify_theme,
#   use_shinyOAuth(),
#   div(
#     class = "pt-4 pb-5",
#     uiOutput("oauth_error"),
#     conditionalPanel(
#       condition = "output.isAuthenticated",
#       layout_sidebar(
#         sidebar = sidebar(
#           card(
#             class = "control-card",
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("sliders-h"),
#               span("Personalize view")
#             )),
#             card_body(
#               selectInput(
#                 "time_range",
#                 "Listening window",
#                 choices = c(
#                   "Last 4 weeks" = "short_term",
#                   "Last 6 months" = "medium_term",
#                   "All-time favorites" = "long_term"
#                 ),
#                 selected = "short_term"
#               ),
#               sliderInput(
#                 "top_limit",
#                 "Top items",
#                 min = 5,
#                 max = 20,
#                 value = 10,
#                 step = 1
#               )
#             ),
#             card_footer(tags$small(
#               class = "text-muted",
#               "Adjust filters to explore different eras of your listening."
#             ))
#           ),
#           width = 320,
#           open = TRUE
#         ),
#         fillable = TRUE,
#         layout_column_wrap(
#           width = "350px",
#           heights_equal = "row",
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("user"),
#               span("Profile")
#             )),
#             card_body(uiOutput("profile"))
#           ),
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("play-circle"),
#               span("Listening sessions")
#             )),
#             card_body(uiOutput("summary_boxes"))
#           ),
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("broadcast-tower"),
#               span("Now playing")
#             )),
#             card_body(uiOutput("now_playing"))
#           )
#         ),
#         layout_column_wrap(
#           width = "400px",
#           fill = TRUE,
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("music"),
#               span("Top tracks")
#             )),
#             card_body(DTOutput("top_tracks"))
#           ),
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("users"),
#               span("Top artists")
#             )),
#             card_body(DTOutput("top_artists"))
#           )
#         ),
#         layout_column_wrap(
#           width = NULL,
#           fill = TRUE,
#           style = css(grid_template_columns = "3fr 2fr"),
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("history"),
#               span("Recent plays")
#             )),
#             card_body(DTOutput("recent"))
#           ),
#           card(
#             card_header(div(
#               class = "d-flex align-items-center gap-2",
#               icon("chart-bar"),
#               span("Artists on repeat")
#             )),
#             card_body(plotOutput("recent_artist_plot", height = "400px"))
#           )
#         )
#       )
#     ),
#     conditionalPanel(
#       condition = "!output.isAuthenticated",
#       div(
#         class = "login-hero d-flex justify-content-center align-items-center",
#         card(
#           class = "login-card text-center p-5",
#           card_body(
#             icon("headphones", class = "display-4 mb-3"),
#             h2("Spotify Listening Studio"),
#             p(
#               class = "lead",
#               "Sign in to reveal your personal soundtrack: relive your top tracks, spotlight your favorite artists, and surface the songs you can't stop replaying."
#             ),
#             actionButton(
#               "login",
#               "Sign in with Spotify",
#               class = "btn btn-lg px-4 py-3 mt-2"
#             ),
#             div(
#               class = "mt-3 small",
#               tags$strong("Scopes:"),
#               " user-top-read • user-read-recently-played • user-read-email • user-read-private"
#             )
#           )
#         )
#       )
#     )
#   )
# )
# 
# 
# ## Server ----------------------------------------------------------------------
# 
# server <- function(input, output, session) {
#   # Handle Spotify login -------------------------------------------------------
# 
#   auth <- oauth_module_server("auth", client, auto_redirect = FALSE)
# 
#   # Expose auth state to JS for our conditionalPanel
#   output$isAuthenticated <- shiny::reactive({
#     isTRUE(auth$authenticated)
#   })
#   shiny::outputOptions(output, "isAuthenticated", suspendWhenHidden = FALSE)
# 
#   observeEvent(input$login, {
#     auth$request_login()
#   })
# 
#   observeEvent(input$logout, {
#     req(isTRUE(auth$authenticated))
#     auth$logout()
#   })
# 
#   output$oauth_error <- renderUI({
#     if (!is.null(auth$error)) {
#       msg <- auth$error
#       if (!is.null(auth$error_description)) {
#         msg <- paste0(msg, ": ", auth$error_description)
#       }
#       div(class = "alert alert-danger", role = "alert", msg)
#     }
#   })
# 
#   # Show user profile ----------------------------------------------------------
# 
#   output$profile <- renderUI({
#     req(auth$token)
#     user_info <- auth$token@userinfo
#     if (length(user_info) == 0) {
#       return(div(class = "text-muted", "No user info"))
#     }
# 
#     avatar <- NULL
#     if (
#       !is.null(user_info$images) &&
#         is.data.frame(user_info$images) &&
#         nrow(user_info$images) > 0
#     ) {
#       img_url <- user_info$images$url[[1]]
#       if (!is.null(img_url) && nzchar(img_url)) {
#         avatar <- tags$img(
#           src = img_url,
#           class = "profile-avatar",
#           alt = "User avatar"
#         )
#       }
#     }
# 
#     display_name <- user_info$display_name %||% user_info$id %||% "<unknown>"
# 
#     followers_badge <- NULL
#     if (
#       !is.null(user_info$followers) &&
#         is.list(user_info$followers) &&
#         !is.null(user_info$followers$total)
#     ) {
#       followers_badge <- span(
#         class = "badge bg-success-subtle text-success-emphasis",
#         "Followers:",
#         tags$span(
#           class = "ms-1",
#           format(user_info$followers$total, big.mark = ",")
#         )
#       )
#     }
# 
#     plan_badge <- NULL
#     if (!is.null(user_info$product)) {
#       plan_badge <- span(
#         class = "badge badge-plan",
#         paste("Plan:", user_info$product)
#       )
#     }
# 
#     country_badge <- NULL
#     if (!is.null(user_info$country)) {
#       country_badge <- span(
#         class = "badge bg-dark border border-success",
#         paste("Country:", user_info$country)
#       )
#     }
# 
#     spotify_link <- NULL
#     if (
#       !is.null(user_info$external_urls) &&
#         is.list(user_info$external_urls) &&
#         !is.null(user_info$external_urls$spotify)
#     ) {
#       spotify_link <- a(
#         icon("external-link-alt", class = "ms-2"),
#         href = user_info$external_urls$spotify,
#         class = "text-decoration-none text-success",
#         target = "_blank",
#         title = "Open in Spotify"
#       )
#     }
# 
#     tagList(
#       div(
#         class = "d-flex align-items-center gap-3 flex-wrap",
#         avatar,
#         div(
#           h4(class = "mb-1", display_name, spotify_link),
#           if (!is.null(user_info$email)) {
#             span(class = "text-muted", user_info$email)
#           }
#         ),
#         div(
#           class = "ms-auto",
#           actionButton(
#             "logout",
#             "Sign out",
#             class = "btn btn-spotify-outline btn-sm"
#           )
#         )
#       ),
#       hr(class = "border-success-subtle"),
#       div(
#         class = "d-flex flex-wrap gap-2",
#         followers_badge,
#         plan_badge,
#         country_badge
#       )
#     )
#   })
# 
#   # Reactives containing Spotify data ------------------------------------------
# 
#   # Data fetch reactives
#   top_tracks <- reactive({
#     req(auth$token, input$time_range, input$top_limit)
#     try(
#       get_top_tracks(
#         auth$token,
#         limit = input$top_limit,
#         time_range = input$time_range
#       ),
#       silent = FALSE
#     )
#   })
# 
#   top_artists <- reactive({
#     req(auth$token, input$time_range, input$top_limit)
#     try(
#       get_top_artists(
#         auth$token,
#         limit = input$top_limit,
#         time_range = input$time_range
#       ),
#       silent = FALSE
#     )
#   })
# 
#   recent <- reactive({
#     req(auth$token)
#     try(get_recently_played(auth$token, limit = 50), silent = FALSE)
#   })
# 
#   summary_data <- reactive({
#     tracks_df <- safe_df(top_tracks())
#     artists_df <- safe_df(top_artists())
#     recent_df <- safe_df(recent())
# 
#     list(
#       top_track = if (!is.null(tracks_df)) {
#         list(
#           name = tracks_df$name[1] %||% "—",
#           artist = tracks_df$artist[1] %||% "—"
#         )
#       } else {
#         NULL
#       },
#       top_artist = if (!is.null(artists_df)) {
#         list(
#           name = artists_df$name[1] %||% "—",
#           genres = if (
#             !is.null(artists_df$genres[1]) && nzchar(artists_df$genres[1])
#           ) {
#             artists_df$genres[1]
#           } else {
#             "—"
#           }
#         )
#       } else {
#         NULL
#       },
#       last_play = if (!is.null(recent_df)) {
#         list(
#           track = recent_df$track[1] %||% "—",
#           artist = recent_df$artist[1] %||% "—",
#           played_at = recent_df$played_at[1]
#         )
#       } else {
#         NULL
#       },
#       unique_recent = if (!is.null(recent_df)) {
#         dplyr::n_distinct(recent_df$artist)
#       } else {
#         NA_integer_
#       }
#     )
#   })
# 
#   # Summary cards --------------------------------------------------------------
# 
#   # These show a few different summary stats about the user's listening
# 
#   output$summary_boxes <- renderUI({
#     data <- summary_data()
# 
#     top_track <- data$top_track
#     top_artist <- data$top_artist
#     last_play <- data$last_play
# 
#     top_track_name <- if (!is.null(top_track)) top_track$name else "—"
#     top_track_artist <- if (!is.null(top_track)) {
#       top_track$artist
#     } else {
#       "No data for this window"
#     }
# 
#     top_artist_name <- if (!is.null(top_artist)) top_artist$name else "—"
#     top_artist_genres <- if (!is.null(top_artist)) {
#       top_artist$genres
#     } else {
#       "No genres available"
#     }
# 
#     last_track_name <- if (!is.null(last_play)) last_play$track else "—"
#     last_track_details <- if (!is.null(last_play)) {
#       parts <- c(last_play$artist %||% "—")
#       if (!is.null(last_play$played_at) && !is.na(last_play$played_at)) {
#         parts <- c(parts, format(last_play$played_at, "%b %d • %H:%M", tz = ""))
#       }
#       paste(parts, collapse = "  |  ")
#     } else {
#       "No recent playback"
#     }
# 
#     unique_recent <- data$unique_recent
#     unique_recent_value <- if (!is.na(unique_recent)) unique_recent else "—"
# 
#     layout_column_wrap(
#       width = "220px",
#       value_box(
#         title = "Top Track",
#         value = top_track_name,
#         showcase = icon("music"),
#         p(class = "text-muted", top_track_artist)
#       ),
#       value_box(
#         title = "Top Artist",
#         value = top_artist_name,
#         showcase = icon("star"),
#         p(class = "text-muted", top_artist_genres)
#       ),
#       value_box(
#         title = "Recent Session",
#         value = last_track_name,
#         showcase = icon("clock"),
#         p(class = "text-muted", last_track_details)
#       ),
#       value_box(
#         title = "Unique Artists (recent)",
#         value = unique_recent_value,
#         showcase = icon("users"),
#         p(class = "text-muted", "Across your latest 50 plays")
#       )
#     )
#   })
# 
#   # Top tracks -----------------------------------------------------------------
# 
#   # Shows the user's top tracks in a data table
# 
#   output$top_tracks <- renderDT({
#     df <- top_tracks()
#     shiny::validate(
#       need(!inherits(df, "try-error"), "Failed to load top tracks"),
#       need(!is.null(df) && nrow(df) > 0, "No tracks returned for this window")
#     )
# 
#     # Calculate play counts from recent plays
#     recent_df <- safe_df(recent())
#     if (!is.null(recent_df)) {
#       recent_df$key <- paste0(recent_df$track, " — ", recent_df$artist)
#       df$key <- paste0(df$name, " — ", df$artist)
#       play_counts <- table(recent_df$key)
#       df$plays <- vapply(
#         df$key,
#         function(k) {
#           count <- suppressWarnings(play_counts[k])
#           if (is.na(count)) 0L else as.integer(count)
#         },
#         integer(1)
#       )
#     } else {
#       df$plays <- 0L
#     }
# 
#     # Drop rows that are entirely missing name & artist
#     keep <- (!is.na(df$name) & nzchar(df$name)) |
#       (!is.na(df$artist) & nzchar(df$artist))
#     df <- df[keep, , drop = FALSE]
# 
#     df <- df[, c("name", "artist", "album", "plays", "popularity")]
#     df$plays <- ifelse(df$plays > 0, sprintf("🔁 %d", df$plays), "—")
#     df$popularity <- ifelse(
#       is.na(df$popularity),
#       "—",
#       sprintf("⭐ %d", round(df$popularity))
#     )
# 
#     # Add rank numbers
#     df <- cbind(`#` = seq_len(nrow(df)), df)
# 
#     df <- stats::setNames(
#       df,
#       c("#", "Track", "Artist", "Album", "Recent Plays", "Popularity")
#     )
#     datatable(
#       df,
#       rownames = FALSE,
#       escape = FALSE,
#       options = list(
#         pageLength = 10,
#         lengthChange = FALSE,
#         order = list(list(0, 'asc')),
#         columnDefs = list(
#           list(orderable = FALSE, targets = 0)
#         )
#       )
#     )
#   })
# 
#   # Top artists ----------------------------------------------------------------
# 
#   # Shows the user's top artists in a data table
# 
#   output$top_artists <- renderDT({
#     df <- top_artists()
#     shiny::validate(
#       need(!inherits(df, "try-error"), "Failed to load top artists"),
#       need(!is.null(df) && nrow(df) > 0, "No artists returned for this window")
#     )
#     df <- df[, c("name", "genres", "popularity", "followers")]
#     df$genres[df$genres == ""] <- "—"
#     df$genres <- vapply(
#       df$genres,
#       function(g) {
#         if (nchar(g) > 50) paste0(substr(g, 1, 47), "...") else g
#       },
#       character(1)
#     )
#     df$popularity <- ifelse(
#       is.na(df$popularity),
#       "—",
#       sprintf("⭐ %d", round(df$popularity))
#     )
#     df$followers <- ifelse(
#       is.na(df$followers),
#       "—",
#       paste0("👥 ", format(round(df$followers), big.mark = ","))
#     )
# 
#     # Add rank numbers
#     df <- cbind(`#` = seq_len(nrow(df)), df)
# 
#     df <- stats::setNames(
#       df,
#       c("#", "Artist", "Genres", "Popularity", "Followers")
#     )
#     datatable(
#       df,
#       rownames = FALSE,
#       escape = FALSE,
#       options = list(
#         pageLength = 10,
#         lengthChange = FALSE,
#         order = list(list(0, 'asc')),
#         columnDefs = list(
#           list(orderable = FALSE, targets = 0)
#         )
#       )
#     )
#   })
# 
#   # Recent plays ---------------------------------------------------------------
# 
#   # Shows the user's recent plays in a data table
# 
#   output$recent <- renderDT({
#     df <- recent()
#     shiny::validate(
#       need(!inherits(df, "try-error"), "Failed to load recent plays"),
#       need(!is.null(df) && nrow(df) > 0, "No recent plays available")
#     )
#     df$played <- format(df$played_at, "%b %d • %H:%M", tz = "")
#     df <- df[, c("played", "track", "artist", "album")]
# 
#     # Add rank numbers
#     df <- cbind(`#` = seq_len(nrow(df)), df)
#     df <- stats::setNames(df, c("#", "Played", "Track", "Artist", "Album"))
#     datatable(
#       df,
#       rownames = FALSE,
#       options = list(
#         pageLength = 10,
#         lengthChange = FALSE,
#         order = list(list(0, 'desc')),
#         columnDefs = list(
#           list(orderable = FALSE, targets = 0)
#         )
#       )
#     )
#   })
# 
#   # Recent artists plot --------------------------------------------------------
# 
#   # Bar plot of most frequently played artists in recent plays
# 
#   output$recent_artist_plot <- renderPlot({
#     df_recent <- recent()
#     shiny::validate(
#       need(!inherits(df_recent, "try-error"), "Failed to load recent plays"),
#       need(
#         !is.null(df_recent) && nrow(df_recent) > 0,
#         "No recent plays available"
#       )
#     )
# 
#     # Primary: counts from recent plays
#     counts <- sort(table(df_recent$artist), decreasing = TRUE)
#     counts_df <- data.frame(
#       artist = names(counts),
#       plays = as.numeric(counts),
#       stringsAsFactors = FALSE
#     )
# 
#     # If the recent signal is weak (<= 3 artists or max <= 1), fall back to time-range top artists by popularity
#     use_fallback <- nrow(counts_df) <= 3 ||
#       max(counts_df$plays, na.rm = TRUE) <= 1
#     if (isTRUE(use_fallback)) {
#       df_top <- safe_df(top_artists())
#       if (!is.null(df_top) && nrow(df_top) > 0) {
#         counts_df <- df_top[, c("name", "popularity")]
#         names(counts_df) <- c("artist", "plays")
#       }
#     }
# 
#     # Take top 10 and order for plotting
#     counts_df <- utils::head(
#       counts_df[order(counts_df$plays, decreasing = TRUE), ],
#       10L
#     )
#     counts_df$artist <- factor(counts_df$artist, levels = rev(counts_df$artist))
# 
#     x_lab <- if (isTRUE(use_fallback)) "Popularity" else "Plays (last 50)"
# 
#     ggplot(counts_df, aes_string(x = "plays", y = "artist")) +
#       geom_col(fill = "#1DB954", width = 0.65) +
#       geom_text(aes(label = plays), hjust = -0.2, color = "#F5F6F8", size = 4) +
#       scale_x_continuous(expand = expansion(mult = c(0, 0.08))) +
#       labs(x = x_lab, y = NULL) +
#       theme_minimal(base_family = "Inter", base_size = 13) +
#       theme(
#         plot.background = element_rect(fill = "#181818", colour = NA),
#         panel.background = element_rect(fill = "#181818", colour = NA),
#         panel.grid.major.y = element_blank(),
#         panel.grid.major.x = element_line(colour = "#FFFFFF22"),
#         text = element_text(colour = "#F5F6F8"),
#         axis.text.y = element_text(colour = "#F5F6F8", size = 12),
#         axis.text.x = element_text(colour = "#F5F6F8", size = 11),
#         plot.margin = margin(10, 20, 10, 20)
#       )
#   })
# 
#   # Now playing ----------------------------------------------------------------
# 
#   # Shows the user's currently playing track with a progress bar
# 
#   output$now_playing <- renderUI({
#     req(auth$token)
#     # refresh every 5 seconds
#     invalidateLater(5000, session)
#     playing <- try(get_currently_playing(auth$token), silent = FALSE)
#     if (inherits(playing, "try-error") || is.null(playing)) {
#       return(div(class = "text-muted", "Nothing playing right now"))
#     }
# 
#     pct <- NA_real_
#     if (
#       !is.na(playing$progress_ms) &&
#         !is.na(playing$duration_ms) &&
#         playing$duration_ms > 0
#     ) {
#       pct <- max(
#         0,
#         min(100, round(playing$progress_ms / playing$duration_ms * 100))
#       )
#     }
# 
#     progress_bar <- NULL
#     if (!is.na(pct)) {
#       progress_bar <- div(
#         class = "progress mt-2",
#         div(
#           class = "progress-bar bg-success",
#           role = "progressbar",
#           style = paste0("width: ", pct, "%"),
#           `aria-valuenow` = pct,
#           `aria-valuemin` = 0,
#           `aria-valuemax` = 100
#         )
#       )
#     }
# 
#     time_label <- span(
#       class = "small text-muted",
#       paste(format_ms(playing$progress_ms), "/", format_ms(playing$duration_ms))
#     )
# 
#     tagList(
#       div(
#         class = "d-flex gap-3 align-items-center",
#         if (!is.null(playing$art)) {
#           tags$img(
#             src = playing$art,
#             class = "now-playing-art",
#             alt = "Album art"
#           )
#         },
#         div(
#           div(class = "fw-semibold", playing$track),
#           div(class = "text-muted", paste(playing$artist, "•", playing$album))
#         )
#       ),
#       progress_bar,
#       div(class = "d-flex justify-content-end", time_label)
#     )
#   })
# }
# 
# 
# # Run app ----------------------------------------------------------------------
# 
# shiny::runApp(
#   shinyApp(ui, server), port = 8100,
#   launch.browser = FALSE
# )
# 
# # Open the app in your regular browser at http://127.0.0.1:8100
# # (viewers in RStudio/Positron/etc. cannot perform necessary redirects)

