Machine Coding Problem

Music Playlist Manager

macoAllmediadoubly-linked-list-logic
Commonly Asked By:SpotifyAppleAmazon

Functional Scope (In-Scope)

  • Constant-time O(1) Operations: Supports adding, removing, and re-linking list nodes in constant time using combined DLL and Map indexing.
  • Repeat Playback State Machine: Handles standard traversal variants (None, Single-track repeat, Full-list wrapping repeat) smoothly.
  • Stateless Shuffle Controller: Integrates shuffle mode using pointer indices, allowing quick toggle actions without corrupting the base DLL hierarchy.
  • O(1) Track Movement API: Re-links pointers between target and destination nodes instantly to handle drag-and-drop operations efficiently.

Explicit Boundaries (Out-of-Scope)

  • Concrete Core Audio Decoders: Omits real-time audio playback engines, network stream buffers, and codecs.
  • GUI Elements & Waveform Graphics: Simulates active track indices instead of writing browser rendering code, player control interfaces, or volume sliders.

Production reference implementations demonstrating doubly linked lists, O(1) removals, re-linking, and playlist traversal states in Java and Python:

// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

class Song {
    private final String id;
    private final String title;
    private final String artist;
    private final int durationSeconds;

    public Song(String id, String title, String artist, int durationSeconds) {
        this.id = id;
        this.title = title;
        this.artist = artist;
        this.durationSeconds = durationSeconds;
    }

    public String getId() { return id; }
    public String getTitle() { return title; }
    public String getArtist() { return artist; }
    public int getDurationSeconds() { return durationSeconds; }

    @Override
    public String toString() {
        return title + " - " + artist;
    }
}

class PlaylistNode {
    Song song;
    PlaylistNode prev;
    PlaylistNode next;

    public PlaylistNode(Song song) {
        this.song = song;
    }
}

enum RepeatMode {
    NONE, ONE, ALL
}

enum ShuffleMode {
    OFF, SHUFFLE
}

class PlaylistManager {
    private PlaylistNode head;
    private PlaylistNode tail;
    private PlaylistNode current;
    private final Map<String, PlaylistNode> nodeMap = new HashMap<>();
    private final ReentrantLock lock = new ReentrantLock();

    private RepeatMode repeatMode = RepeatMode.NONE;
    private ShuffleMode shuffleMode = ShuffleMode.OFF;
    private final List<String> shuffleOrder = new ArrayList<>();
    private int shuffleIndex = -1;

    // O(1) Add to end
    public void addSong(Song song) {
        lock.lock();
        try {
            if (nodeMap.containsKey(song.getId())) {
                throw new IllegalArgumentException("Song already exists in playlist.");
            }

            PlaylistNode newNode = new PlaylistNode(song);
            nodeMap.put(song.getId(), newNode);

            if (head == null) {
                head = newNode;
                tail = newNode;
                current = newNode; // Set pointer if first song
            } else {
                tail.next = newNode;
                newNode.prev = tail;
                tail = newNode;
            }

            if (shuffleMode == ShuffleMode.SHUFFLE) {
                shuffleOrder.add(song.getId());
            }
        } finally {
            lock.unlock();
        }
    }

    // O(1) Delete node
    public void removeSong(String songId) {
        lock.lock();
        try {
            PlaylistNode node = nodeMap.get(songId);
            if (node == null) return;

            // If removing the currently playing node, advance current pointer
            if (node == current) {
                current = getNextNodeLogical();
            }

            // Unlink node
            if (node.prev != null) {
                node.prev.next = node.next;
            } else {
                head = node.next; // Head removed
            }

            if (node.next != null) {
                node.next.prev = node.prev;
            } else {
                tail = node.prev; // Tail removed
            }

            nodeMap.remove(songId);
            shuffleOrder.remove(songId);

            // Adjust shuffle index
            if (shuffleMode == ShuffleMode.SHUFFLE) {
                if (shuffleOrder.isEmpty()) {
                    shuffleIndex = -1;
                    current = null;
                } else {
                    shuffleIndex = Math.min(shuffleIndex, shuffleOrder.size() - 1);
                }
            }
        } finally {
            lock.unlock();
        }
    }

    // O(1) Move node to a different position
    public void moveBefore(String songIdToMove, String targetSongId) {
        lock.lock();
        try {
            if (songIdToMove.equals(targetSongId)) return;

            PlaylistNode toMove = nodeMap.get(songIdToMove);
            PlaylistNode target = nodeMap.get(targetSongId);

            if (toMove == null || target == null) {
                throw new IllegalArgumentException("Song node parameters not found.");
            }

            // 1. Unlink toMove from its current position
            if (toMove.prev != null) {
                toMove.prev.next = toMove.next;
            } else {
                head = toMove.next;
            }

            if (toMove.next != null) {
                toMove.next.prev = toMove.prev;
            } else {
                tail = toMove.prev;
            }

            // 2. Link toMove before target
            PlaylistNode prevOfTarget = target.prev;

            toMove.next = target;
            target.prev = toMove;
            toMove.prev = prevOfTarget;

            if (prevOfTarget != null) {
                prevOfTarget.next = toMove;
            } else {
                head = toMove; // target was head
            }
        } finally {
            lock.unlock();
        }
    }

    public Song getCurrentlyPlaying() {
        lock.lock();
        try {
            return (current != null) ? current.song : null;
        } finally {
            lock.unlock();
        }
    }

    // Next track based on Repeat and Shuffle states
    public Song next() {
        lock.lock();
        try {
            if (current == null) return null;

            if (repeatMode == RepeatMode.ONE) {
                return current.song; // Repeat one: stay on current
            }

            PlaylistNode nextNode = getNextNodeLogical();
            if (nextNode != null) {
                current = nextNode;
                return current.song;
            }
            
            current = null; // Playlist ended
            return null;
        } finally {
            lock.unlock();
        }
    }

    // Previous track
    public Song prev() {
        lock.lock();
        try {
            if (current == null) return null;

            if (repeatMode == RepeatMode.ONE) {
                return current.song;
            }

            PlaylistNode prevNode = getPrevNodeLogical();
            if (prevNode != null) {
                current = prevNode;
                return current.song;
            }

            current = null;
            return null;
        } finally {
            lock.unlock();
        }
    }

    public void setRepeatMode(RepeatMode mode) {
        lock.lock();
        try {
            this.repeatMode = mode;
        } finally {
            lock.unlock();
        }
    }

    public void toggleShuffle() {
        lock.lock();
        try {
            if (shuffleMode == ShuffleMode.OFF) {
                shuffleMode = ShuffleMode.SHUFFLE;
                generateShuffleOrder();
            } else {
                shuffleMode = ShuffleMode.OFF;
                shuffleOrder.clear();
                shuffleIndex = -1;
            }
        } finally {
            lock.unlock();
        }
    }

    private PlaylistNode getNextNodeLogical() {
        if (shuffleMode == ShuffleMode.SHUFFLE) {
            if (shuffleOrder.isEmpty()) return null;
            
            shuffleIndex++;
            if (shuffleIndex >= shuffleOrder.size()) {
                if (repeatMode == RepeatMode.ALL) {
                    shuffleIndex = 0; // Wrap around permutation
                } else {
                    return null; // Stop
                }
            }
            return nodeMap.get(shuffleOrder.get(shuffleIndex));
        } else {
            if (current.next != null) {
                return current.next;
            } else if (repeatMode == RepeatMode.ALL) {
                return head; // Wrap around DLL
            }
            return null;
        }
    }

    private PlaylistNode getPrevNodeLogical() {
        if (shuffleMode == ShuffleMode.SHUFFLE) {
            if (shuffleOrder.isEmpty()) return null;

            shuffleIndex--;
            if (shuffleIndex < 0) {
                if (repeatMode == RepeatMode.ALL) {
                    shuffleIndex = shuffleOrder.size() - 1;
                } else {
                    return null;
                }
            }
            return nodeMap.get(shuffleOrder.get(shuffleIndex));
        } else {
            if (current.prev != null) {
                return current.prev;
            } else if (repeatMode == RepeatMode.ALL) {
                return tail;
            }
            return null;
        }
    }

    private void generateShuffleOrder() {
        shuffleOrder.clear();
        shuffleOrder.addAll(nodeMap.keySet());
        Collections.shuffle(shuffleOrder);

        if (current != null) {
            shuffleIndex = shuffleOrder.indexOf(current.song.getId());
        } else {
            shuffleIndex = 0;
        }
    }

    public List<Song> getOrderedPlaylist() {
        lock.lock();
        try {
            List<Song> songs = new ArrayList<>();
            PlaylistNode runner = head;
            while (runner != null) {
                songs.add(runner.song);
                runner = runner.next;
            }
            return songs;
        } finally {
            lock.unlock();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== JAVA MUSIC PLAYLIST DEMO ===");
        PlaylistManager manager = new PlaylistManager();

        Song s1 = new Song("1", "Bohemian Rhapsody", "Queen", 354);
        Song s2 = new Song("2", "Hotel California", "Eagles", 390);
        Song s3 = new Song("3", "Stairway to Heaven", "Led Zeppelin", 482);
        Song s4 = new Song("4", "Imagine", "John Lennon", 183);

        manager.addSong(s1);
        manager.addSong(s2);
        manager.addSong(s3);
        manager.addSong(s4);

        System.out.println("Playlist Order: " + manager.getOrderedPlaylist());
        System.out.println("Currently playing: " + manager.getCurrentlyPlaying());

        System.out.println("Skip next: " + manager.next());
        System.out.println("Skip next: " + manager.next());

        System.out.println("Moving Imagine (s4) before Bohemian Rhapsody (s1)...");
        manager.moveBefore("4", "1");
        System.out.println("New Playlist Order: " + manager.getOrderedPlaylist());

        System.out.println("Setting Repeat ALL and skipping next...");
        manager.setRepeatMode(RepeatMode.ALL);
        System.out.println("Skip next: " + manager.next());
        System.out.println("Skip next: " + manager.next());

        System.out.println("Toggling Shuffle...");
        manager.toggleShuffle();
        System.out.println("Skip next in shuffle: " + manager.next());

        System.out.println("Removing Stairway to Heaven (s3)...");
        manager.removeSong("3");
        System.out.println("New Playlist Order: " + manager.getOrderedPlaylist());
        System.out.println("=== END OF DEMO ===");
    }
}