Music playlists, shuffled in an orderly manner

I have somewhat weird question. Let’s say I have several playlists and need to to play random music in such way:

while true:
 play random track from playlist A
 play random track from playlist B

How do I make it?
It is easy to play one playlist with “shuffle” option, but I need random track from playlists A and B in turns. Making such playlist by hand will make it predictable for me at least, while rolling dices for every track seems beyond madness.
No music player to my knowledge has such option available, so my guess is I need something to create orderly playlist from shuffled playlists.

(I host tabletop game for my guests and want background music to follow this pattern to create sound atmosphere)

Here you go. This should work for both playlists or folders full of music. I only tested folders full of music because I don’t have any playlists handy.

Just save this as alternate_player.py then edit the playlist_a_path and playlist_b_path run with python3 ./alternate_player.py.

import os
import random
import subprocess
import sys
import time

# --- Configuration ---
# Option 1: Playlists as directories
playlist_a_path = '/home/alan/Music/The Gripping Adventures of Space Walrus'
playlist_b_path = '/home/alan/Music/Lofi/Lone Wolf'
use_directories = True # Set to True if paths above are directories

# Option 2: Playlists as M3U files
# playlist_a_path = '/path/to/your/playlist_A.m3u'
# playlist_b_path = '/path/to/your/playlist_B.m3u'
# use_directories = False # Set to False if paths above are .m3u files

music_extensions = ('.mp3', '.ogg', '.flac', '.m4a', '.wav', '.opus') # Add others if needed
player_command = 'ffplay' # Or 'mplayer', 'mpv', 'cvlc' etc.
player_options = '-autoexit' # use '--play-and-exit' for cvlc
# --- End Configuration ---

def get_files_from_directory(dir_path):
    """Gets a list of music files from a directory."""
    files = []
    try:
        for filename in os.listdir(dir_path):
            if filename.lower().endswith(music_extensions):
                files.append(os.path.join(dir_path, filename))
    except FileNotFoundError:
        print(f"Error: Directory not found: {dir_path}", file=sys.stderr)
    return files

def get_files_from_m3u(m3u_path):
    """Gets a list of music files from an M3U playlist file."""
    files = []
    try:
        with open(m3u_path, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                # Skip comments and empty lines
                if line and not line.startswith('#'):
                    # Basic check if it looks like a file path and has a valid extension
                    # M3U can contain URLs, this script assumes local files.
                    # Might need refinement if your M3U uses relative paths differently.
                    if os.path.exists(line) and line.lower().endswith(music_extensions):
                         files.append(line)
                    elif os.path.exists(os.path.join(os.path.dirname(m3u_path), line)) and line.lower().endswith(music_extensions):
                        # Handle relative paths relative to the M3U file location
                        files.append(os.path.join(os.path.dirname(m3u_path), line))

    except FileNotFoundError:
        print(f"Error: Playlist file not found: {m3u_path}", file=sys.stderr)
    except Exception as e:
        print(f"Error reading playlist {m3u_path}: {e}", file=sys.stderr)
    return files

def play_random_track(file_list, playlist_name):
    """Selects and plays a random track from the list."""
    if not file_list:
        print(f"Warning: Playlist '{playlist_name}' is empty or contains no valid music files.", file=sys.stderr)
        time.sleep(1) # Prevent busy loop if both are empty
        return False

    track_to_play = random.choice(file_list)
    print(f"Playing from {playlist_name}: {os.path.basename(track_to_play)}")

    try:
        # Use subprocess.run which waits for the command to complete
        # Adjust command based on player if needed (e.g., VLC and ffplay needs specific flags)
        cmd = [player_command, player_options, track_to_play]

        result = subprocess.run(cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) # check=False to handle player errors gracefully

        if result.returncode != 0:
            print(f"Warning: Player exited with error code {result.returncode} for track {track_to_play}.", file=sys.stderr)
            print(f"Player stderr: {result.stderr.decode()}", file=sys.stderr)
            # Decide if you want to stop on error or just continue
            # return False # Uncomment to stop script on player error

    except FileNotFoundError:
        print(f"Error: Player command '{player_command}' not found. Is it installed and in your PATH?", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error during playback: {e}", file=sys.stderr)
        # return False # Uncomment to stop script on other playback errors
    return True


# --- Main Execution ---
print("Loading playlists...")
if use_directories:
    playlist_a_files = get_files_from_directory(playlist_a_path)
    playlist_b_files = get_files_from_directory(playlist_b_path)
else:
    playlist_a_files = get_files_from_m3u(playlist_a_path)
    playlist_b_files = get_files_from_m3u(playlist_b_path)

if not playlist_a_files and not playlist_b_files:
     print("Error: Both playlists are empty or could not be read. Exiting.", file=sys.stderr)
     sys.exit(1)
elif not playlist_a_files:
     print("Warning: Playlist A is empty. Will only play from Playlist B.", file=sys.stderr)
elif not playlist_b_files:
     print("Warning: Playlist B is empty. Will only play from Playlist A.", file=sys.stderr)


print("Starting playback loop (Press Ctrl+C to stop)...")
try:
    while True:
        # Play from Playlist A
        play_random_track(playlist_a_files, "Playlist A")

        # Play from Playlist B
        play_random_track(playlist_b_files, "Playlist B")

except KeyboardInterrupt:
    print("\nPlayback stopped by user.")
    sys.exit(0)

Works here, using ffplay (part of ffmpeg):

$ python alternate_player.py
Loading playlists...
Starting playback loop (Press Ctrl+C to stop)...
Playing from Playlist A: Diversity Mix.mp3
Playing from Playlist B: 18.The Startup.mp3
Playing from Playlist A: DJ Space Walrus - RoughEdit.mp3
Playing from Playlist B: 04.Breath of Anything.mp3
^C
Playback stopped by user.
3 Likes

Thanks, it works perfect.

2 Likes

Excellent. I am envious of your tabletop parties! :smiley:

1 Like