Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Artist Data

livebooks/01_artist.livemd

Artist Data

Mix.install([:faker, :fuzzy_compare])

Section

# Define a function that generates a single artist
generate_artist = fn () ->
  # Generate a random timestamp in the past 30 days
  created_at = Faker.DateTime.backward(30)  

  # Ensure `updated_at` is always after `created_at`
  updated_at = Faker.DateTime.between(created_at, DateTime.utc_now())  

  # Return an artist map with generated data
  %{
    id: Faker.UUID.v4(),            # Unique identifier
    name: Faker.Person.name(),       # Generates a realistic artist name
    biography: Faker.Lorem.sentence(), # Short artist description
    slug: nil,                       # Will be generated later
    created_at: created_at,          # Timestamp when created
    updated_at: updated_at           # Timestamp when last updated
  }
end

# Call the function once to preview the result
# The `.()` is required because `generate_artist` is an anonymous function.
generate_artist.()

# In Livebook, the last expression in a cell is automatically displayed.
# Since `generate_artist.()` returns a map, Livebook shows that map as output.
%{
  id: "e65ff327-2085-41e2-ab0c-8cf7c93688cd",
  name: "Issac Watsica PhD",
  slug: nil,
  created_at: ~U[2025-02-09 05:24:31.120567Z],
  updated_at: ~U[2025-03-04 14:11:04.832686Z],
  biography: "Voluptatem dolor ab repellat quam dolores."
}
# Generate a list of 8 fake artists
# `Enum.map/2` takes a range (1 to 8) and applies the function to each number.
# We use `_` because the actual number isn't needed—just the loop count.
artists = Enum.map(1..8, fn _ -> 
  # Call `generate_artist.()` to create a new artist in each iteration
  generate_artist.() 
end)

# Livebook automatically displays the last evaluated expression.
# Since `artists` is a list of maps, it will be rendered as a table in Livebook.
artists
[
  %{
    id: "86ccb26c-eac7-45ea-8b67-4eabc101b368",
    name: "Ewald Price",
    slug: nil,
    created_at: ~U[2025-02-17 15:07:26.947503Z],
    updated_at: ~U[2025-03-06 13:14:25.054757Z],
    biography: "Nihil laborum magnam aut ipsum id neque voluptatem eos ex."
  },
  %{
    id: "a218fa30-1474-43c5-864a-d512b41d1666",
    name: "Jewell Bruen",
    slug: nil,
    created_at: ~U[2025-02-18 14:38:52.887512Z],
    updated_at: ~U[2025-02-28 20:43:57.944533Z],
    biography: "Recusandae voluptatem delectus accusantium ab sapiente fuga."
  },
  %{
    id: "2f8505ba-6163-4a5c-82bd-899433879be2",
    name: "Modesta Prohaska",
    slug: nil,
    created_at: ~U[2025-02-19 09:16:01.407243Z],
    updated_at: ~U[2025-03-01 09:12:15.523373Z],
    biography: "Ipsam et voluptatum non ducimus voluptatem autem vero quaerat?"
  },
  %{
    id: "aa05a01a-d635-4fd9-a685-bebb84db887b",
    name: "Arthur Jacobs",
    slug: nil,
    created_at: ~U[2025-02-10 21:53:54.933469Z],
    updated_at: ~U[2025-03-04 08:46:51.162192Z],
    biography: "Est illum tempore ipsa adipisci!"
  },
  %{
    id: "4c052a3a-ffec-4c7a-a1f2-2f71681272e4",
    name: "Ms. Berneice Williamson III",
    slug: nil,
    created_at: ~U[2025-03-03 07:48:28.305352Z],
    updated_at: ~U[2025-03-04 09:53:05.656641Z],
    biography: "Temporibus eius saepe repudiandae debitis aut possimus rem."
  },
  %{
    id: "f4054ba6-0409-48f1-891a-683473485e40",
    name: "Vincent Erdman",
    slug: nil,
    created_at: ~U[2025-03-01 09:36:01.091945Z],
    updated_at: ~U[2025-03-02 20:39:48.621278Z],
    biography: "Autem rerum quia sapiente provident."
  },
  %{
    id: "896ab668-cea2-4dac-85fb-bb24ce6ab6f5",
    name: "Nash Dicki IV",
    slug: nil,
    created_at: ~U[2025-02-27 06:38:38.163815Z],
    updated_at: ~U[2025-03-01 15:21:16.492619Z],
    biography: "Est est id at cupiditate nam quia eos?"
  },
  %{
    id: "3a6c5a1d-43e3-4fa7-b5eb-14045f19ae52",
    name: "Cathryn Beier",
    slug: nil,
    created_at: ~U[2025-03-03 07:04:18.094176Z],
    updated_at: ~U[2025-03-07 04:30:11.507846Z],
    biography: "Voluptatem quam ea ut sunt illo."
  }
]
# Function to generate a URL-friendly slug from an artist name
slugify = fn (name) ->
  name
  |> :unicode.characters_to_nfd_binary()      # Normalize characters (e.g., "ö" → "o")
  |> String.replace(~r/[\p{Mn}]/u, "")        # Remove diacritical marks
  |> String.downcase()                        # Convert to lowercase
  |> String.replace(~r/[^a-z0-9]+/, "-")      # Replace non-alphanumeric chars with "-"
  |> String.trim("-")                         # Remove leading/trailing hyphens
end

# Example usage: Generate a slug for a band with special characters
slugify.("Mötley Crüe!")
"motley-crue"
# Import ExUnit to use assert in Livebook
import ExUnit.Assertions

# Utility module for artist-related helper functions
defmodule ArtistUtils do
  # Function to generate a URL-friendly slug from an artist name
  def slugify(name) do
    name
    |> :unicode.characters_to_nfd_binary()      # Normalize characters (e.g., "ö" → "ö")
    |> String.replace(~r/[\p{Mn}]/u, "")        # Remove diacritical marks
    |> String.downcase()                        # Convert to lowercase
    |> String.replace(~r/[^a-z0-9]+/, "-")      # Replace non-alphanumeric chars with "-"
    |> String.trim("-")                         # Remove leading/trailing hyphens
  end

  # Function to generate a random `created_at` timestamp in the past 30 days
  def fake_created_at() do
    Faker.DateTime.backward(30)
  end

  # Function to generate an `updated_at` timestamp that is always after `created_at`
  def fake_updated_at(created_at) do
    Faker.DateTime.between created_at, DateTime.utc_now()
  end
end

# ✅ Test slugify function
assert ArtistUtils.slugify("Mötley Crüe!") == "motley-crue"
assert ArtistUtils.slugify("AC/DC") == "ac-dc"
assert ArtistUtils.slugify("Guns N' Roses") == "guns-n-roses"

# ✅ Test fake_created_at function
created_at = ArtistUtils.fake_created_at()
assert DateTime.compare(created_at, DateTime.utc_now()) == :lt  # created_at must be in the past

# ✅ Test fake_updated_at function using the previously generated `created_at`
updated_at = ArtistUtils.fake_updated_at(created_at)
assert DateTime.compare(updated_at, created_at) == :gt  # updated_at must be after created_at

# ✅ If all assertions pass, return success message
"All ArtistUtils tests passed!"
"All ArtistUtils tests passed!"
defmodule ArtistFactory do
  # Function to create an artist, generating a fake name and biography if none are provided
  def create_artist(name \\ Faker.Person.name(), biography \\ Faker.Lorem.sentence()) do
    # Generate timestamps using ArtistUtils
    created_at = ArtistUtils.fake_created_at()
    updated_at = ArtistUtils.fake_updated_at(created_at)

    # Generate a slug from the artist name
    slug = ArtistUtils.slugify(name)

    # Return an artist map with structured data
    %{
      id: Faker.UUID.v4(),    # Unique identifier
      name: name,             # Provided or generated name
      biography: biography,   # Provided or generated biography
      slug: slug,             # Generated slug
      created_at: created_at, # Timestamp when created
      updated_at: updated_at  # Timestamp when last updated
    }
  end
end

# ✅ Test `create_artist/1` with a provided name and biography
ArtistFactory.create_artist("Mötley Crüe!", "Heavy metal legends")

# ✅ Test `create_artist/1` with a provided name but a fake biography
ArtistFactory.create_artist("Nirvana")

# ✅ Test `create_artist/1` with a completely random artist
ArtistFactory.create_artist()
%{
  id: "43c0129f-1ca3-47bf-8fc9-c35c2775d976",
  name: "Carlotta Grant",
  slug: "carlotta-grant",
  created_at: ~U[2025-03-04 11:08:28.512139Z],
  updated_at: ~U[2025-03-07 17:55:51.454098Z],
  biography: "Enim consequatur nihil quia qui facere reiciendis voluptatum sint."
}
# Generate a list of 8 fake artists using ArtistFactory
artists = Enum.map(1..8, fn _ -> ArtistFactory.create_artist() end)

# Create a map where the slug is the key, and the artist struct is the value
artist_map = Enum.reduce(artists, %{}, fn artist, acc ->
  Map.put(acc, artist.slug, artist)
end)

artist_map
%{
  "abdullah-hickle" => %{
    id: "5966b386-5edd-496d-9619-1a9114a6b258",
    name: "Abdullah Hickle",
    slug: "abdullah-hickle",
    created_at: ~U[2025-02-21 01:08:09.195340Z],
    updated_at: ~U[2025-02-21 23:33:09.972654Z],
    biography: "Officiis pariatur sunt modi amet."
  },
  "favian-roberts" => %{
    id: "413cc74b-d363-43ba-b58c-7bd579eeed15",
    name: "Favian Roberts",
    slug: "favian-roberts",
    created_at: ~U[2025-02-06 02:00:27.436379Z],
    updated_at: ~U[2025-02-13 10:09:01.980977Z],
    biography: "Dolorum libero quod ut fugit ea incidunt qui?"
  },
  "hertha-maggio" => %{
    id: "89ef7fb3-413e-495a-8094-a33b3c33b7c5",
    name: "Hertha Maggio",
    slug: "hertha-maggio",
    created_at: ~U[2025-02-26 14:05:52.695687Z],
    updated_at: ~U[2025-03-07 06:59:35.143694Z],
    biography: "Aliquid eveniet excepturi voluptates."
  },
  "jody-jakubowski" => %{
    id: "2dd3cd97-99f7-45f3-9c2d-df37eed7dea5",
    name: "Jody Jakubowski",
    slug: "jody-jakubowski",
    created_at: ~U[2025-02-12 00:43:24.017708Z],
    updated_at: ~U[2025-02-24 04:49:32.062391Z],
    biography: "Ut dolores aliquid corporis quos."
  },
  "milford-beahan" => %{
    id: "1825a60c-91df-42ff-b278-67f2149286de",
    name: "Milford Beahan",
    slug: "milford-beahan",
    created_at: ~U[2025-02-07 02:20:55.287223Z],
    updated_at: ~U[2025-02-11 22:55:10.496631Z],
    biography: "Tempora quibusdam debitis qui officia distinctio!"
  },
  "monica-hahn" => %{
    id: "a786ac9a-25f9-4a4b-93ae-e34ee63a5f70",
    name: "Monica Hahn",
    slug: "monica-hahn",
    created_at: ~U[2025-03-01 23:44:29.368410Z],
    updated_at: ~U[2025-03-07 21:57:43.512727Z],
    biography: "Est saepe id assumenda non vitae qui!"
  },
  "mrs-catherine-lakin" => %{
    id: "a72920df-506f-4757-8c1b-c2a135fcf4d0",
    name: "Mrs. Catherine Lakin",
    slug: "mrs-catherine-lakin",
    created_at: ~U[2025-02-11 06:41:04.950593Z],
    updated_at: ~U[2025-02-23 19:53:40.228718Z],
    biography: "Maiores velit quasi explicabo maxime harum perferendis et accusamus."
  },
  "nedra-watsica" => %{
    id: "e9e08e66-6f04-48e7-a992-4179e6a5bc5f",
    name: "Nedra Watsica",
    slug: "nedra-watsica",
    created_at: ~U[2025-02-09 16:01:54.327194Z],
    updated_at: ~U[2025-02-23 04:09:45.824234Z],
    biography: "Incidunt minima officia omnis!"
  }
}
# Function to retrieve an artist by their slug from the artist map
get_artist_by_slug = fn (artist_map, slug) ->
  Map.get(artist_map, slug)
end

# Example usage: Pick the first slug from the map and fetch the corresponding artist
first_slug = List.first(Map.keys(artist_map))
retrieved_artist = get_artist_by_slug.(artist_map, first_slug)

# Display the retrieved artist
retrieved_artist
%{
  id: "5966b386-5edd-496d-9619-1a9114a6b258",
  name: "Abdullah Hickle",
  slug: "abdullah-hickle",
  created_at: ~U[2025-02-21 01:08:09.195340Z],
  updated_at: ~U[2025-02-21 23:33:09.972654Z],
  biography: "Officiis pariatur sunt modi amet."
}
# Function to retrieve an artist by their ID from the artist map
get_artist_by_id = fn (artist_map, id) ->
  Map.get(artist_map, id)
end


# Simulating an API request where we get an artist ID from user input
# user_provided_id = "id-no-existing"
user_provided_id = "929abf58-b737-4bf7-b34f-caa6dd4d74a1"  # This would typically come from an API request

# Retrieve the artist using the provided ID
retrieved_artist = get_artist_by_id.(artist_map, user_provided_id)

# Handle the case where the artist is not found
if retrieved_artist do
  {:ok, retrieved_artist}
else
  available_ids = artist_map |> Map.values() |> Enum.map(& &1.id)

  error_map = %{
    error_message: "Artist not found for ID: #{user_provided_id}",
    available_artist_ids: available_ids
  }

  {:error, error_map}
end
{:error,
 %{
   error_message: "Artist not found for ID: 929abf58-b737-4bf7-b34f-caa6dd4d74a1",
   available_artist_ids: ["5966b386-5edd-496d-9619-1a9114a6b258",
    "413cc74b-d363-43ba-b58c-7bd579eeed15", "89ef7fb3-413e-495a-8094-a33b3c33b7c5",
    "2dd3cd97-99f7-45f3-9c2d-df37eed7dea5", "1825a60c-91df-42ff-b278-67f2149286de",
    "a786ac9a-25f9-4a4b-93ae-e34ee63a5f70", "a72920df-506f-4757-8c1b-c2a135fcf4d0",
    "e9e08e66-6f04-48e7-a992-4179e6a5bc5f"]
 }}
artist_map
%{
  "abdullah-hickle" => %{
    id: "5966b386-5edd-496d-9619-1a9114a6b258",
    name: "Abdullah Hickle",
    slug: "abdullah-hickle",
    created_at: ~U[2025-02-21 01:08:09.195340Z],
    updated_at: ~U[2025-02-21 23:33:09.972654Z],
    biography: "Officiis pariatur sunt modi amet."
  },
  "favian-roberts" => %{
    id: "413cc74b-d363-43ba-b58c-7bd579eeed15",
    name: "Favian Roberts",
    slug: "favian-roberts",
    created_at: ~U[2025-02-06 02:00:27.436379Z],
    updated_at: ~U[2025-02-13 10:09:01.980977Z],
    biography: "Dolorum libero quod ut fugit ea incidunt qui?"
  },
  "hertha-maggio" => %{
    id: "89ef7fb3-413e-495a-8094-a33b3c33b7c5",
    name: "Hertha Maggio",
    slug: "hertha-maggio",
    created_at: ~U[2025-02-26 14:05:52.695687Z],
    updated_at: ~U[2025-03-07 06:59:35.143694Z],
    biography: "Aliquid eveniet excepturi voluptates."
  },
  "jody-jakubowski" => %{
    id: "2dd3cd97-99f7-45f3-9c2d-df37eed7dea5",
    name: "Jody Jakubowski",
    slug: "jody-jakubowski",
    created_at: ~U[2025-02-12 00:43:24.017708Z],
    updated_at: ~U[2025-02-24 04:49:32.062391Z],
    biography: "Ut dolores aliquid corporis quos."
  },
  "milford-beahan" => %{
    id: "1825a60c-91df-42ff-b278-67f2149286de",
    name: "Milford Beahan",
    slug: "milford-beahan",
    created_at: ~U[2025-02-07 02:20:55.287223Z],
    updated_at: ~U[2025-02-11 22:55:10.496631Z],
    biography: "Tempora quibusdam debitis qui officia distinctio!"
  },
  "monica-hahn" => %{
    id: "a786ac9a-25f9-4a4b-93ae-e34ee63a5f70",
    name: "Monica Hahn",
    slug: "monica-hahn",
    created_at: ~U[2025-03-01 23:44:29.368410Z],
    updated_at: ~U[2025-03-07 21:57:43.512727Z],
    biography: "Est saepe id assumenda non vitae qui!"
  },
  "mrs-catherine-lakin" => %{
    id: "a72920df-506f-4757-8c1b-c2a135fcf4d0",
    name: "Mrs. Catherine Lakin",
    slug: "mrs-catherine-lakin",
    created_at: ~U[2025-02-11 06:41:04.950593Z],
    updated_at: ~U[2025-02-23 19:53:40.228718Z],
    biography: "Maiores velit quasi explicabo maxime harum perferendis et accusamus."
  },
  "nedra-watsica" => %{
    id: "e9e08e66-6f04-48e7-a992-4179e6a5bc5f",
    name: "Nedra Watsica",
    slug: "nedra-watsica",
    created_at: ~U[2025-02-09 16:01:54.327194Z],
    updated_at: ~U[2025-02-23 04:09:45.824234Z],
    biography: "Incidunt minima officia omnis!"
  }
}
real_artists = %{
  "the-beatles" => %{id: "1", name: "The Beatles", genre: "Rock"},
  "michael-jackson" => %{id: "2", name: "Michael Jackson", genre: "Pop"},
  "madonna" => %{id: "3", name: "Madonna", genre: "Pop"},
  "led-zeppelin" => %{id: "4", name: "Led Zeppelin", genre: "Rock"},
  "beyonce" => %{id: "5", name: "Beyoncé", genre: "R&B"},
  "bob-dylan" => %{id: "6", name: "Bob Dylan", genre: "Folk"},
  "taylor-swift" => %{id: "7", name: "Taylor Swift", genre: "Country/Pop"},
  "kendrick-lamar" => %{id: "8", name: "Kendrick Lamar", genre: "Hip-Hop"}
}

defmodule ArtistLookup do
  alias FuzzyCompare.SortedChunks

  def get_artist_by_slug(artists, slug) do
    case Map.get(artists, slug) do
      nil -> 
        available_slugs = Map.keys(artists)
        {:error, %{error_message: "Artist not found for slug: #{slug}", available_artist_slugs: available_slugs}}
      
      artist -> {:ok, artist}
    end
  end

  def suggest_closest_slug(artists, slug) do
    case ArtistLookup.get_artist_by_slug(artists, slug) do
      {:ok, artist} ->
        {:ok, artist}

      {:error, error_info} ->
        closest_match = find_closest_match(slug, error_info.available_artist_slugs)

        updated_error_info =
          if closest_match do
            Map.put(error_info, :suggested_slug, closest_match)
          else
            error_info
          end

        {:error, updated_error_info}
    end
  end

  defp find_closest_match(_slug, []), do: nil

  defp find_closest_match(slug, slugs) do
    slugs
    |> Enum.map(fn s -> {s, SortedChunks.substring_similarity(slug, s)} end)
    |> Enum.max_by(fn {_slug, score} -> score end)
    |> case do
      {best_slug, score} when score > 0.7 -> best_slug
      _ -> nil
    end
  end
end


artist_slug = "jk"


case ArtistLookup.suggest_closest_slug(real_artists, artist_slug) do
  {:ok, artist} -> IO.puts("Found: #{artist.name}, Genre: #{artist.genre}")

  {:error, error_info} ->
    IO.puts("#{error_info.error_message}")

    # Only show suggestion if it exists
    if Map.has_key?(error_info, :suggested_slug) do
      IO.puts("Did you mean: #{error_info.suggested_slug}?")
    end
end
Artist not found for slug: jk
Did you mean: michael-jackson?
:ok

Album Data

defmodule AlbumFactory do
  # Function to create an album, requiring an `artist_id`
  def create_album(artist_id, name \\ Faker.Lorem.sentence(), year_released \\ Faker.random_between(1950, Date.utc_today().year)) do
    created_at = ArtistUtils.fake_created_at()
    updated_at = ArtistUtils.fake_updated_at(created_at)

    %{
      id: Faker.UUID.v4(),    # Unique identifier
      name: name,             # Provided or generated name
      artist_id: artist_id,   # Links to an existing artist
      year_released: year_released,
      created_at: created_at,
      updated_at: updated_at
    }
  end
end

# ✅ Create an artist first
artist = ArtistFactory.create_artist()

# ✅ Create an album linked to the generated artist
album = AlbumFactory.create_album(artist.id, "Dark Side of the Moon")

# ✅ Create another album for the same artist with a random name
random_album = AlbumFactory.create_album(artist.id)

# ✅ Return a tuple containing both albums
albums_tuple = {album, random_album}

# ✅ Display the result in Livebook
albums_tuple
{%{
   id: "c436ce21-3bfd-4244-995d-1fcd114915e6",
   name: "Dark Side of the Moon",
   created_at: ~U[2025-02-18 21:01:43.554533Z],
   updated_at: ~U[2025-02-25 06:57:58.137039Z],
   artist_id: "bba3bd88-6bab-43e3-a76c-ec08bff3ff7d",
   year_released: 1991
 },
 %{
   id: "7fc8e79b-46a2-410d-9584-8616c6ae64a3",
   name: "Soluta accusantium labore ut?",
   created_at: ~U[2025-02-22 08:30:45.002402Z],
   updated_at: ~U[2025-03-07 21:26:02.392366Z],
   artist_id: "bba3bd88-6bab-43e3-a76c-ec08bff3ff7d",
   year_released: 1975
 }}