Files
freeCodeCamp/curriculum/challenges/english/blocks/lab-playlist-remix-engine/6976db7ecfa770bf21307b20.md
T
VuBui217 1cf34f9696 feat(curriculum): add lab playlist remix engine (#65515)
Co-authored-by: Dario <105294544+Dario-DC@users.noreply.github.com>
Co-authored-by: Ilenia M <nethleen@gmail.com>
Co-authored-by: majestic-owl448 <26656284+majestic-owl448@users.noreply.github.com>
Co-authored-by: Jessica Wilkins <67210629+jdwilkin4@users.noreply.github.com>
Co-authored-by: jdwilkin4 <jwilkin4@hotmail.com>
2026-04-21 11:05:15 +02:00

11 KiB

id, title, challengeType, dashedName
id title challengeType dashedName
6976db7ecfa770bf21307b20 Build a Playlist Remix Engine 26 build-a-playlist-remix-engine

--description--

In this lab, you will build a program that creates a single remix playlist from multiple playlists submitted by listeners.

Each listener provides a list of songs they want to hear. Some songs may appear more than once, and some artists may show up too many times. Your job is to work through these playlists step by step: combine them into one list, score each song, remove duplicate songs, limit how often the same artist appears, and then create a final play order.

Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.

User Stories:

  1. You should create a function named flattenPlaylists that accepts an array of playlists where each playlist is an array of objects with the following properties: trackId, artist, title, votes, bpm. If the input is not an array, flattenPlaylists should return an empty array. An example playlist has been provided for you. You can use this example to test out your function.

  2. flattenPlaylists should return a flat array of track objects, where each object includes all the original track properties plus a source property set to an array with the playlist index and the track index indicating where the track originated.

  3. You should create a function named scoreTracks that accepts an array of track objects as returned by flattenPlaylists (each with trackId, artist, title, votes, bpm, and source properties) and returns a new array of track objects, each with a score property added using the formula: votes * 10 - Math.abs(bpm - 120).

  4. You should create a function named dedupeTracks that accepts an array of track objects as returned by scoreTracks and returns a new array with duplicate trackId entries removed, keeping only the first occurrence of each.

  5. You should create a function named enforceArtistQuota that accepts an array of track objects as returned by dedupeTracks and a number representing the maximum allowed occurrences per artist. The function should return a new array where no artist appears more times than the given number, keeping the earliest occurrences.

  6. You should create a function named buildSchedule that accepts an array of track objects as returned by enforceArtistQuota and returns a new array of { slot, trackId } objects, where slot is a 1-based index representing each track's position in the broadcast order.

  7. You should create a function named remixPlaylist that accepts an array of playlists and the maximum number of allowed occurrences per artist. The function should return the final broadcast schedule as an array of { slot, trackId } objects, by calling flattenPlaylists, scoreTracks, dedupeTracks, enforceArtistQuota, and buildSchedule in order.

--hints--

You should have a function named flattenPlaylists.

assert.isFunction(flattenPlaylists);

You should return an empty array from flattenPlaylists when the input is not an array.

assert.deepEqual(flattenPlaylists(null), []);
assert.deepEqual(flattenPlaylists(undefined), []);
assert.deepEqual(flattenPlaylists({}), []);
assert.deepEqual(flattenPlaylists("not an array"), []);
assert.deepEqual(flattenPlaylists(123), []);

Each track returned by flattenPlaylists should include a source field that is an array containing the playlist index and the track index.

const playlists = [
  [
    { trackId: "t1", artist: "A", title: "Song 1", votes: 2, bpm: 120 },
    { trackId: "t2", artist: "B", title: "Song 2", votes: 1, bpm: 110 }
  ],
  [{ trackId: "t3", artist: "C", title: "Song 3", votes: 5, bpm: 130 }]
];

const flat = flattenPlaylists(playlists);

assert.isArray(flat);
assert.lengthOf(flat, 3);

flat.forEach((t) => {
  assert.property(t, "source");
  assert.isArray(t.source);
  assert.lengthOf(t.source, 2);
  assert.isNumber(t.source[0]);
  assert.isNumber(t.source[1]);
});

assert.deepEqual(flat[0].source, [0, 0]);
assert.deepEqual(flat[1].source, [0, 1]);
assert.deepEqual(flat[2].source, [1, 0]);

You should have a function named scoreTracks.

assert.isFunction(scoreTracks)

Each track returned by scoreTracks should include a numeric score field.

const tracks = [
  { trackId: "t1", artist: "A", title: "Song 1", votes: 2, bpm: 120, source: [0, 0] }
];

const scored = scoreTracks(tracks);

assert.isArray(scored);
assert.lengthOf(scored, 1);

assert.property(scored[0], "score");
assert.isNumber(scored[0].score);

You should calculate score using a target BPM of 120 and this formula: votes * 10 - Math.abs(bpm - 120).

const tracks = [
  { trackId: "t1", artist: "A", title: "Song 1", votes: 3, bpm: 120, source: [0, 0] }, // 30
  { trackId: "t2", artist: "B", title: "Song 2", votes: 1, bpm: 100, source: [0, 1] },  // -10
  { trackId: "t3", artist: "C", title: "Song 3", votes: 2, bpm: 135, source: [1, 0] }  // 5
];

const formulaScored = scoreTracks(tracks);

const s1 = formulaScored.find(t => t.trackId === "t1").score;
const s2 = formulaScored.find(t => t.trackId === "t2").score;
const s3 = formulaScored.find(t => t.trackId === "t3").score;

assert.equal(s1, 30);
assert.equal(s2, -10);
assert.equal(s3, 5);

You should have a function named dedupeTracks.

assert.isFunction(dedupeTracks)

When duplicate trackId values exist, dedupeTracks should keep only the first occurrence of the track.

const dupes = [
  { trackId: "t1", artist: "A", title: "Song 1", votes: 1, bpm: 120, source: [0, 0], score: 10 },
  { trackId: "t2", artist: "B", title: "Song 2", votes: 2, bpm: 119, source: [0, 1], score: 19 },
  { trackId: "t1", artist: "A", title: "Song 3", votes: 9, bpm: 140, source: [1, 0], score: 70 }
];

const deduped = dedupeTracks(dupes);

assert.isArray(deduped);
assert.lengthOf(deduped, 2);

assert.equal(deduped[0].trackId, "t1");
assert.equal(deduped[0].title, "Song 1");
assert.equal(deduped[1].trackId, "t2");
assert.equal(deduped[1].title, "Song 2");

You should have a function named enforceArtistQuota.

assert.isFunction(enforceArtistQuota)

enforceArtistQuota should ensure no artist appears more than maxPerArtist times by removing extra tracks while keeping the earliest ones.

const quotaTracks = [
  { trackId: "t1", artist: "A", title: "Song 1", votes: 1, bpm: 120, source: [0, 0], score: 10 },
  { trackId: "t2", artist: "A", title: "Song 2", votes: 1, bpm: 121, source: [0, 1], score: 9 },
  { trackId: "t3", artist: "B", title: "Song 3", votes: 1, bpm: 118, source: [0, 2], score: 8 },
  { trackId: "t4", artist: "A", title: "Song 4", votes: 1, bpm: 110, source: [1, 0], score: 0 }
];

const limited = enforceArtistQuota(quotaTracks, 2);

let artistACount = 0;

for (const track of limited) {
  if (track.artist === "A") {
    artistACount++;
  }
}
assert.isAtMost(artistACount, 2);
assert.notInclude(limited.map(t => t.trackId), "t4");

You should have a function named buildSchedule.

assert.isFunction(buildSchedule);

buildSchedule should return an array of objects with the shape { slot, trackId }, where slot starts at 1.

const scheduleInput = [{ trackId: "t1" }, { trackId: "t2" }, { trackId: "t3" }];
const schedule = buildSchedule(scheduleInput);

assert.lengthOf(schedule, 3);

schedule.forEach((item, index) => {
  assert.hasAllKeys(item, ["slot", "trackId"]);
  assert.equal(item.slot, index + 1);
  assert.equal(item.trackId, scheduleInput[index].trackId);
});

You should have a function named remixPlaylist.

assert.isFunction(remixPlaylist)

remixPlaylist should call the helper functions in order to produce the final schedule.

assert.lengthOf(remixPlaylist, 2);

const testPlaylists = [
  [
    { trackId: "t1", artist: "A", title: "Song 1", votes: 2, bpm: 120 },
    { trackId: "t1", artist: "A", title: "Duplicate", votes: 9, bpm: 140 }
  ],
  [
    { trackId: "t2", artist: "A", title: "Song 2", votes: 1, bpm: 110 },
    { trackId: "t3", artist: "B", title: "Song 3", votes: 4, bpm: 118 }
  ]
];

const expected = buildSchedule(enforceArtistQuota(dedupeTracks(scoreTracks(flattenPlaylists(testPlaylists))),1));

const actual = remixPlaylist(testPlaylists, 1);

assert.deepEqual(actual, expected);

--seed--

--seed-contents--

const playlists = [
  [
    {
      trackId: "trk101",
      artist: "Velvet Comet",
      title: "Crimson Afterglow",
      votes: 5,
      bpm: 122
    },
    {
      trackId: "trk102",
      artist: "Neon Harbor",
      title: "Static Horizon",
      votes: 2,
      bpm: 108
    },
    {
      trackId: "trk103",
      artist: "Lunar Arcade",
      title: "Midnight Frequency",
      votes: 4,
      bpm: 128
    }
  ],
  [
    {
      trackId: "trk201",
      artist: "Solar Echo",
      title: "Glass Skyline",
      votes: 3,
      bpm: 115
    },
    {
      trackId: "trk202",
      artist: "Velvet Comet",
      title: "Satellite Hearts",
      votes: 6,
      bpm: 124
    }
  ]
];

--solutions--

function flattenPlaylists(playlists) {
  if (!Array.isArray(playlists)) return [];

  const result = [];

  for (let i = 0; i < playlists.length; i++) {
    const playlist = playlists[i];
    if (!Array.isArray(playlist)) continue;

    for (let j = 0; j < playlist.length; j++) {
      const track = playlist[j];
      result.push({
        trackId: track.trackId,
        artist: track.artist,
        title: track.title,
        votes: track.votes,
        bpm: track.bpm,
        source: [i, j]
      });
    }
  }
  return result;
}

function scoreTracks(tracks) {
  const targetBpm = 120;
  const result = [];

  for (let i = 0; i < tracks.length; i++) {
    const track = tracks[i];

    result.push({
      trackId: track.trackId,
      artist: track.artist,
      title: track.title,
      votes: track.votes,
      bpm: track.bpm,
      source: track.source,
      score: (track.votes * 10) - Math.abs(track.bpm - targetBpm)
    });
  }
  return result;
}

function dedupeTracks(tracks) {
  const seenTrackIds = [];
  const result = [];

  for (let i = 0; i < tracks.length; i++) {
    if (!seenTrackIds.includes(tracks[i].trackId)) {
      seenTrackIds.push(tracks[i].trackId);
      result.push(tracks[i]);
    }
  }
  return result;
}

function enforceArtistQuota(tracks, maxPerArtist) {
  const artists = [];
  const artistCounts = [];
  const result = [];

  for (let i = 0; i < tracks.length; i++) {
    const artist = tracks[i].artist;
    const artistIndex = artists.indexOf(artist);

    if (artistIndex === -1) {
      artists.push(artist);
      artistCounts.push(1);
      result.push(tracks[i]);
    } else if (artistCounts[artistIndex] < maxPerArtist) {
      artistCounts[artistIndex]++;
      result.push(tracks[i]);
    }
  }

  return result;
}

function buildSchedule(tracks) {
  const schedule = [];

  for (let i = 0; i < tracks.length; i++) {
    schedule.push({
      slot: i + 1,
      trackId: tracks[i].trackId
    });
  }

  return schedule;
}

function remixPlaylist(playlists, maxPerArtist) {
  const flattened = flattenPlaylists(playlists);
  const scored = scoreTracks(flattened);
  const deduped = dedupeTracks(scored);
  const limited = enforceArtistQuota(deduped, maxPerArtist);
  return buildSchedule(limited);
}