KEXP is a radio station in Seattle that I’ve listened to regularly almost every day for the past twenty years. Since I live in Minneapolis I use their streaming service to listen via their app, my Sonos or a desktop player. During the work day I via my desktop VLC player where the track information is not available. I actually have a pretty great .pls file that contains all the best streaming radio stations from around the world – but that’s not what I’m writing about today. Streaming the station outside the app doesn’t provide any playlist information or what song is currently being played. So, if I hear a song I like or one that I don’t know the title to I have to open a new browser tab or entirely new window, browse to kexp.org and see what the currently playing track is from their home page widget. This lead me to create a KEXP now playing browser extension for Firefox and Chrome.

The browser extension is very simple and does one thing: when the user clicks the extension in their browser it hits KEXP’s public API playing endpoint, returns the now playing track, artist and album information, then displays that information right there in a neat browser fly out.

Code

The browser extension is built in React and JavaScript. There are two main parts to the code, getting the data inside the useEffect React Hook inside the main App component and a component called <NowPlaying /> (along with child components) that do the presentation of the now playing data.

Below you can see there are two fetch requests to the KEXP API. The first request is to the /plays endpoint to return the full list of recently played tracks. There is not a way to get just the currently playing track, so I grab the first item from the array which turns out to be the most recent item. Then I parse the object to get all the necessary information include the show_uri. The second request is to the show_uri endpoint to return what show is currently playing.

After the two requests the NowPlaying.js component breaks down the returned object to render in the extension window. The rendered information is the show title and host in the header along with album artwork along with the artist, track album and KEXP comment that was included with the track displayed in the body.

App.js

function App() {
  const [nowPlaying, setNowPlaying] = useState(null);

  useEffect(() => {
    fetch("https://api.kexp.org/v2/plays/")
      .then((response) => response.json())
      .then(data => {

        if (data.results && data.results.length > 0) {
          let results = data.results;
          let trackData = results[0];
          let nowPlaying = {
            thumbnailUri: trackData.thumbnail_uri,
            album: trackData.album,
            artist: trackData.artist,
            song: trackData.song,
            playType: trackData.play_type,
            comment: trackData.comment,
            showTitle: "",
            showDJs:[]
          };

          let showUri = trackData.show_uri;
          fetch(showUri)
            .then(response => response.json())
            .then((showData => {
              nowPlaying.showTitle = showData.program_name;
              nowPlaying.showDJs = showData.host_names;
              setNowPlaying(nowPlaying);
            }));
        };
      })

  }, []);

  return (
    <div className="wrapper">
      <NowPlaying nowPlaying={nowPlaying} />
    </div>
  );
}

NowPlaying.js

Parent component to parse apart the nowPlaying parameter. Consists of header, thumbnail and track information.

function NowPlaying(props) {
    let nowPlaying = props.nowPlaying;
        return (
            <>
                <div className="header">
                    <Header showTitle={nowPlaying.showTitle} showDJs={nowPlaying.showDJs} />
                </div>
                <div className="content">
                    <div className="left">
                        <Thumbnail
                            thumbnailUri={nowPlaying.thumbnailUri}
                            album={nowPlaying.album}
                        />
                    </div>
                    <div className="right">
                        <TrackInfo
                            artist={nowPlaying.artist}
                            song={nowPlaying.song}
                            album={nowPlaying.album}
                            playType={nowPlaying.playType}
                        />
                    </div>
                </div>
                <div className="comment">
                    <Comment
                        comment={nowPlaying.comment}
                    />
                </div>
            </>
        )
}

export default NowPlaying;

Header.js

Display the header title bar that includes the show name and host. Part of this title includes a link to the KEXP donation page.

function Header(props) {
    let djText = "";
    console.log(props.showDJs)
    if (props.showDJs) {
        djText = props.showDJs.map((item, i) => {
            let name = item;
            if (i !== 0) {
                name = "/" + name;
            }
            return name;
        });
    }
    return (
        <div className="title">
            <a href="https://www.kexp.org/donate/" alt="Support the music">{props.showTitle ? props.showTitle : ""} {djText ? `with ${djText}` : ""}</a>
        </div>
    )
}

export default Header;

Thumbnail.js

This component displays the thumbnail image if there is one present in the parent response. If there isn’t one available I’m displaying a placeholder image.

import placeHolder from "../images/placeholder.png";

function Thumbnail(props) {
  
    if (props.thumbnailUri) {
      return <img src={props.thumbnailUri} alt={props.album} />
    }

    return <img src={placeHolder} alt="Now playing placeholder" />
}

export default Thumbnail;

TrackInfo.js

HTML breakdown of the track information. Checking if the track is actually an airbreak, otherwise display the track information.

function TrackInfo(props) {
    
    if (props.playType === "airbreak") {
        return (
            <div className="block">
                Air Break
            </div>
        )
    }
    return (
        <>
            <div className="block">
                <div className="label">
                    Song:
            </div>
                <div className="text">
                    {props.song}
                </div>
            </div>
            <div className="block">
                <div className="label">
                    Artist:
                </div>
                <div className="text">
                    {props.artist}
                </div>
            </div>
            <div className="block">
                <div className="label">
                    Album:
                </div>
                <div className="text">
                    {props.album}
                </div>
            </div>
        </>
    )
}

export default TrackInfo;

Download

The browser extension is available for both Firefox and Chrome as well as source code available in Github. Download and install from the links below.

Firefox | Chrome | Github