NHL Finns Project Update

2024-10-16

This post describes updates made to the NHL Finns Phoenix LiveView project. It also discusses reasons for working on side projects and explains how Oban is used for background jobs in this project.

It’s that time of year again—the NHL hockey season has begun. I previously wrote about one of my side projects, NHL Finns. The site has received many updates this year, and it’s time for another post.

What is the project about anyway? It provides NHL game results and statistics concentrating on Finnish players. There are player career stats, stats leaders in different categories, contract data, standings, playoff results, etc. It’s all in Finnish, although stats tables are quite readable in any language.

Why Work on Side Projects?

It’s a great way to improve skills and knowledge in many technical areas. Every minute detail is your own responsibility, and nothing will get done if you don’t just pick something up and do it, whether it’s front-end, back-end, CSS, testing or deployment. However, the project must be something you genuinely enjoy working on. It should be something that’s useful, at least to you personally, and hopefully also for other people interested in the topic.

It can be like working on a start-up, where people are not siloed into a specific role altogether, but need to and get to work on different areas based on demands on any given period. Another difference is that you’ll get paid for working in a startup, while side projects don’t guarantee any income and are usually not even expected to. Hence, it is necessary to choose a topic that’s close to your heart.

Tech Stack

One important factor that makes this project fun to work on is the tech stack used to build it. As I wrote earlier, the site was built using Elixir, Phoenix Framework, and Phoenix LiveView. Excellent tooling, great documentation, and the quality of the available libraries are all factors that make this a good choice for projects.

Project Updates

Quite a lot has changed since my previous post from years ago, and here’s a short summary.

Design

The design is more consistent and modern. The templates take more advantage of LiveView functional components, making the components reusable and also consistent in styling. When the original site was implemented, it was a bit of a hit-and-miss, and new things were not necessarily planned all the way through before implementation.

Game Score!

UI Features

This site was originally built so that I could quickly find information from last night’s games. This information naturally includes the game scores, but even more importantly, how the Finnish players did and how many points they scored. This has been one of the main features from the beginning. Small details have been added to show 3-star selections for the games and whether the Finnish players scored in a possible shootout.

Navigation

The site has been converted to use mobile navigation for all screen sizes. This is a bit of a contentious point for me, but at the moment, it feels like the best solution, considering how many of the users come from mobile. There’s also enough space for all the main navigation items that have been added through the years.

Navigation is not only about top-level navigation but also about how to get from one piece of data to the next. This has been improved in many places, and when opening a game score from the home page, you can navigate to the team by clicking the team name or to the player pages by clicking on the names. Or from a player’s draft pick to that year’s draft results.

Architecture improvements

Retrieving Data

The data source has been changed to one that’s more accurate and quicker to update. This required many changes but also allowed me to refactor the code to be nicer and easier to work with.

Background Jobs With Oban

The polling and updating of the data were updated to use the Oban library, allowing more flexibility in defining when and what to poll.

Oban is a robust and feature-rich background job processing library for Elixir applications. It leverages PostgreSQL for reliable job storage and processing, ensuring that tasks are executed efficiently and safely.

Oban has a free version and two paid enterprise packages. The Web package provides a comprehensive web UI showing job states, enabling users to filter down to details.

The Pro version has features that make the background jobs even more powerful, such as rate-limiting and encrypted jobs. You should check the full feature table on the Oban website.

How Does Oban Help This App

Oban also has a Cron plugin, which can be used to trigger jobs at specific times. In this application, it is used to trigger a fetcher module to get today’s games from the DB. It then schedules game pollers to poll the games at a specified interval until they reach a completed state. After the game, some other feeds need to be polled to get more details and stats. The game poller schedules these once the game is completed, and the stats jobs are called recursively until they return expected data.

Data Polling Using Oban!

How does an Oban Worker Look Like?

Here’s an example of a simple worker that polls the standings. Note: hexdocs.com/oban contains thorough documentation of the library.

defmodule Nhl.Workers.StandingsPoller do
  @moduledoc """
  Oban worker to poll NHL standings data.
  """

  alias Nhl.Services.Standings

  use Oban.Worker, queue: :default, max_attempts: 3, unique: true

  @impl Oban.Worker
  def perform(_job) do
    Standings.update()
  end
end

The perform/1 function is called when the job is executed. The worker can do other things and schedule itself or other workers to run. Depending on the result of the operation, the job is determined to be complete or retried following the max_attemts configuration.

This example code below shows how to schedule a new job to in 60 seconds. This can be recursive (schedule itself) or it can schedule a different job.

%{id: game_id}
|> GamePoller.new(schedule_in: 60)
|> Oban.insert()

What happens when the StandingsPoller is run? It calls the Standings.update/1 function, which fetches the standings data from the API and stores in the database. This is an example service module for one of the feeds:

defmodule Nhl.Services.Standings do
  @moduledoc """
  Module to handle standings data updates from the API
  """

  alias Nhl.Context
  alias Nhl.Feed.NHL.Standings

  require Logger

  @spec update(binary()) :: {:ok, :updated} | {:error, binary()}
  def update(date \\ "now") do
    case Standings.fetch(date) do
      {:ok, data} ->
        Logger.info("NHL: Fetched Standings data successfully")
        store_data(data)

      {:error, reason} ->
        Logger.warning("NHL: Failed to fetch Standings data: #{inspect(reason)}")
        {:error, reason}
    end
  end

  defp store_data(data) do
    data
    |> Enum.map(&enrich_attrs/1)
    |> Enum.each(fn standing ->
      case Nhl.Teams.create_or_update_team_stats(standing) do
        {:ok, _updated_standing} ->
          Logger.debug("NHL: Standings updated/created successfully")

        {:error, changeset} ->
          Logger.warning("NHL: Failed to update/create Standings.")
      end
    end)
  end

  defp enrich_attrs(attrs) do
    attrs
    |> Context.add_season_and_year()
    |> Context.add_team_id()
  end
end

Future Direction

Where does this site go in the future? Plenty of things could be implemented. There could be links or embedded videos of scored goals. There might be short posts about the game scores or other news. Or maybe more in-depth stats, as all North American sports leagues love stats and offer hundreds and hundreds of data points. Or an English version of the site, maybe? Let me know what you want to see to make this the best source of information for you.

Conclusion

Feel free to contact me via LinkedIn (link in footer) if you have any questions or want to talk about services related to web projects, whether it be for test automation or web development.

PS.

Here are some links if you want to learn more about some popular Elixir projects:

The following conference videos are also worth checking out: