Prompt for Generating a Spotify Release Tracking Script Using Spotipy

Objective:

Create a Python script that interacts with the Spotify API to track new releases from a user's followed artists and updates a dedicated playlist with these new tracks. The script should ensure that duplicate tracks are not added and maintain a history of processed releases to avoid redundancy in future runs.

Functional Requirements:

    Authentication:
        Use the Spotipy library to authenticate with the Spotify API.
        Utilize OAuth with appropriate scopes to access and modify user data.
        Manage authentication tokens, including refreshing them as needed.

    Data Management:
        Maintain a JSON file (release_history.json) to keep track of processed releases to prevent duplicate processing.
        Implement error handling for file operations and API requests.

    Artist and Release Handling:
        Retrieve all artists that the user is following on Spotify.
        For each followed artist, fetch their latest releases (including albums, EPs, and singles) available in the user's specified country (e.g., Norway).
        Exclude releases categorized as compilations or appearances on other artists' works.
        Filter out releases older than a specified cutoff date (e.g., 30 days) during the first run.

    Playlist Management:
        Check if a playlist named "New Songs" exists in the user's account.
            If it doesn't exist, create the playlist with a specified description and privacy setting.
            If it exists, retrieve all existing track URIs to avoid adding duplicates.
        Add new tracks from recent releases to the "New Songs" playlist.
            Ensure that tracks are added in batches (maximum of 100 tracks per API request) to comply with Spotify's API limitations.
            Insert new tracks at the top of the playlist.

    Utility Functions:
        Remove articles ("A", "An", "The") from artist names for consistent sorting.
        Pluralize words based on count for user-friendly output messages.

    Error Handling and Retries:
        Implement retry logic for API requests that might fail due to network issues or rate limiting.
        Handle exceptions gracefully, providing informative messages to the user.

    Logging and User Feedback:
        Provide clear console outputs indicating the script's progress, such as authentication status, number of artists found, releases processed, tracks added, etc.
        Inform the user of any errors encountered during execution.

Technical Requirements:

    Libraries:
        spotipy: For interacting with the Spotify API.
        requests: To handle HTTP requests and related exceptions.
        json: For reading and writing the release history file.
        os: For file system operations.
        re: For regular expressions used in processing artist names.
        time, datetime: For handling dates, times, and delays between retries.

    Configuration:
        Define constants for playlist name, history file path, cache file path, and required Spotify API scopes.
        Utilize environment variables or a separate configuration file to securely handle sensitive information like client_id, client_secret, and redirect_uri.

    Authentication Scopes:
        playlist-read-private
        playlist-modify-private
        user-follow-read

Additional Considerations:

    First Run Initialization:
        On the initial execution, initialize the release_history.json file if it doesn't exist.
        Set a cutoff date (e.g., 30 days before the current date) to determine which releases qualify as "new."

    Release Date Precision:
        Handle different release date precisions (year, month, day) to ensure accurate date comparisons.

    Batch Processing:
        When adding tracks to the playlist, process them in batches to adhere to Spotify's API rate limits and request size constraints.

    Code Quality:
        Write clean, well-documented, and modular code.
        Use descriptive function and variable names.
        Include docstrings for all functions explaining their purpose and usage.

Sample Script Structure:

Below is an outline of how the script should be organized, including key functions and their responsibilities.

import os
import json
import time
import re
from datetime import datetime, timedelta
from spotipy import Spotify, SpotifyException
from spotipy.oauth2 import SpotifyOAuth
from requests.exceptions import ReadTimeout, ConnectTimeout, ConnectionError

# Constants
PLAYLIST_NAME = "New Songs"
HISTORY_FILE = "release_history.json"
CACHE_FILE = ".cache"
SCOPES = "playlist-read-private playlist-modify-private user-follow-read"

# Maximum number of retries for API requests
MAX_RETRIES = 3
# Sleep time between retries in seconds
RETRY_SLEEP = 2

def remove_articles(name):
    """Remove articles 'A', 'An', 'The' from the beginning of the artist name."""
    return re.sub(r'^(The |An |A )', '', name, flags=re.IGNORECASE).strip()

def pluralize(word, count):
    """Return the word in singular or plural form based on the count."""
    return f"{word}" if count == 1 else f"{word}s"

def authenticate_spotify():
    """Authenticate with Spotify and return a Spotipy client."""
    sp = Spotify(auth_manager=SpotifyOAuth(
        scope=SCOPES,
        cache_path=CACHE_FILE,
        show_dialog=True,
        redirect_uri='http://localhost:8888/callback',  # Replace with your Redirect URI
        client_id='YOUR_CLIENT_ID',        # Replace with your Client ID
        client_secret='YOUR_CLIENT_SECRET' # Replace with your Client Secret
    ))
    return sp

def load_history():
    """Load the release history from a JSON file."""
    if not os.path.exists(HISTORY_FILE):
        return {"processed_releases": []}
    with open(HISTORY_FILE, 'r') as file:
        return json.load(file)

def save_history(history):
    """Save the release history to a JSON file."""
    with open(HISTORY_FILE, 'w') as file:
        json.dump(history, file, indent=4)

def get_followed_artists(sp):
    """Retrieve all followed artists from the user's Spotify account."""
    artists = []
    try:
        results = sp.current_user_followed_artists(limit=50)
        artists.extend(results['artists']['items'])
        while results['artists']['next']:
            results = sp.next(results['artists'])
            artists.extend(results['artists']['items'])
    except SpotifyException as e:
        print(f"Error fetching followed artists: {e}")
    return artists

def get_user_playlist(sp, user_id, playlist_name):
    """Retrieve a playlist by name. Return None if not found."""
    playlists = []
    try:
        results = sp.current_user_playlists(limit=50)
        playlists.extend(results['items'])
        while results['next']:
            results = sp.next(results)
            playlists.extend(results['items'])
    except SpotifyException as e:
        print(f"Error fetching user playlists: {e}")
    for playlist in playlists:
        if playlist['name'].lower() == playlist_name.lower():
            return playlist
    return None

def create_playlist(sp, user_id, name, description="", public=False):
    """Create a new playlist."""
    try:
        playlist = sp.user_playlist_create(user=user_id, name=name, public=public, description=description)
        return playlist
    except SpotifyException as e:
        print(f"Error creating playlist '{name}': {e}")
        return None

def fetch_new_releases(sp, artist_id):
    """Fetch new releases (Albums, EPs, Singles) for a given artist."""
    releases = []
    try:
        results = sp.artist_albums(artist_id, album_type='album,ep,single', country='NO', limit=50)
        releases.extend(results['items'])
        while results['next']:
            results = sp.next(results)
            releases.extend(results['items'])
    except (SpotifyException, ReadTimeout, ConnectTimeout, ConnectionError) as e:
        print(f"Error fetching releases for artist ID {artist_id}: {e}")
    return releases

def fetch_tracks(sp, album_id):
    """Fetch tracks for a given album ID."""
    tracks = []
    try:
        results = sp.album_tracks(album_id, limit=50)
        tracks.extend(results['items'])
        while results['next']:
            results = sp.next(results)
            tracks.extend(results['items'])
    except (SpotifyException, ReadTimeout, ConnectTimeout, ConnectionError) as e:
        print(f"Error fetching tracks for album ID {album_id}: {e}")
    return tracks

def extract_release_date(release):
    """Extract and format the release date."""
    date = release.get('release_date', '0000-00-00')
    precision = release.get('release_date_precision', 'day')
    if precision == 'year':
        return f"{date}-01-01"
    elif precision == 'month':
        return f"{date}-01"
    return date  # Assuming day precision

def main():
    # Authenticate with Spotify
    try:
        sp = authenticate_spotify()
        print("Authenticated with Spotify successfully.")
    except Exception as e:
        print(f"Failed to authenticate with Spotify: {e}")
        return

    # Load or initialize release history
    if not os.path.exists(HISTORY_FILE):
        history = {"processed_releases": []}
        save_history(history)
        print(f"Initialized '{HISTORY_FILE}' with default values.")
        first_run = True
    else:
        history = load_history()
        first_run = False

    # Get current user ID
    try:
        user = sp.current_user()
        user_id = user['id']
    except SpotifyException as e:
        print(f"Error fetching current user: {e}")
        return

    # Retrieve followed artists
    print("Fetching followed artists...")
    artists = []
    for attempt in range(MAX_RETRIES):
        try:
            artists = get_followed_artists(sp)
            break
        except (SpotifyException, ReadTimeout, ConnectTimeout, ConnectionError):
            if attempt < MAX_RETRIES - 1:
                time.sleep(RETRY_SLEEP)
            else:
                print("Failed to fetch followed artists after multiple attempts.")
                return

    if not artists:
        print("Found 0 followed artists.")
        return

    # Remove articles and sort artists alphabetically
    artists_sorted = sorted(artists, key=lambda x: remove_articles(x['name']).lower())
    simplified_artists = [(remove_articles(artist['name']), artist['id']) for artist in artists_sorted]
    artist_count = len(simplified_artists)
    print(f"Found {artist_count} followed {pluralize('artist', artist_count)}.\n")
    print("Processing artists in alphabetical order:")

    new_releases = []

    if first_run:
        cutoff_date = datetime.today() - timedelta(days=30)

    for artist_name, artist_id in simplified_artists:
        print(f"Found 0 new releases from {artist_name}.", end='')

        # Fetch releases
        for attempt in range(MAX_RETRIES):
            try:
                releases = fetch_new_releases(sp, artist_id)
                break
            except (SpotifyException, ReadTimeout, ConnectTimeout, ConnectionError):
                if attempt < MAX_RETRIES - 1:
                    time.sleep(RETRY_SLEEP)
                else:
                    print(f"\nFailed to fetch releases for {artist_name} after multiple attempts.")
                    releases = []

        if not releases:
            print("")
            continue

        # Filter and process releases
        valid_releases = []
        for release in releases:
            # Exclude 'Appears On' and 'Compilations'
            if release['album_group'] not in ['appears_on', 'compilation']:
                release_id = release['id']
                if release_id not in history['processed_releases']:
                    release_date = extract_release_date(release)
                    try:
                        release_date_obj = datetime.strptime(release_date, '%Y-%m-%d')
                    except ValueError:
                        # Handle incorrect date formats
                        print(f"\nInvalid date format for release '{release['name']}' from {artist_name}. Skipping.")
                        # Still mark as processed to avoid retrying
                        history['processed_releases'].append(release_id)
                        continue

                    if first_run and release_date_obj < cutoff_date:
                        # On first run, skip older releases but mark them as processed
                        history['processed_releases'].append(release_id)
                        continue
                    elif first_run and release_date_obj >= cutoff_date:
                        # On first run, add only recent releases
                        tracks = fetch_tracks(sp, release_id)
                        if not tracks:
                            # Skip releases with no tracks
                            history['processed_releases'].append(release_id)
                            continue
                        valid_releases.append({
                            'id': release_id,
                            'name': release['name'],
                            'release_date': release_date,
                            'tracks': tracks
                        })
                    else:
                        # On subsequent runs, process as usual
                        tracks = fetch_tracks(sp, release_id)
                        if not tracks:
                            # Skip releases with no tracks
                            history['processed_releases'].append(release_id)
                            continue
                        valid_releases.append({
                            'id': release_id,
                            'name': release['name'],
                            'release_date': release_date,
                            'tracks': tracks
                        })

        if valid_releases:
            release_count = len(valid_releases)
            print(f"\n{release_count} new {pluralize('release', release_count)} from {artist_name}.")
            for release in valid_releases:
                print(f"    * {release['name']} ({release['release_date']})")
                for track in release['tracks']:
                    new_releases.append({
                        'release_date': release['release_date'],
                        'track_uri': track['uri'],
                        'track_name': track['name']
                    })
                # Add release to history
                history['processed_releases'].append(release['id'])
        else:
            print("")

    if not new_releases:
        print("\nNo new releases found from followed artists.")
    else:
        # Sort new releases by release_date descending (newest first)
        new_releases_sorted = sorted(new_releases, key=lambda x: x['release_date'], reverse=True)

        # Get or create the "New Songs" playlist
        playlist = get_user_playlist(sp, user_id, PLAYLIST_NAME)
        if not playlist:
            print(f"\nCreated new private playlist '{PLAYLIST_NAME}'.")
            playlist = create_playlist(sp, user_id, PLAYLIST_NAME, description="New releases from favorite artists.", public=False)
            if not playlist:
                print("Failed to create the playlist.")
                return
            existing_track_uris = []
        else:
            print(f"\nPlaylist '{PLAYLIST_NAME}' already exists.")
            # Retrieve existing tracks in the playlist
            existing_track_uris = []
            try:
                results = sp.playlist_tracks(playlist['id'], limit=100)
                existing_track_uris.extend([item['track']['uri'] for item in results['items'] if item['track']])
                while results['next']:
                    results = sp.next(results)
                    existing_track_uris.extend([item['track']['uri'] for item in results['items'] if item['track']])
                track_count = len(existing_track_uris)
                print(f"Retrieved {track_count} existing {pluralize('track', track_count)} from the playlist.")
            except SpotifyException as e:
                print(f"Error fetching existing tracks from the playlist: {e}")
                return

        # Filter out tracks that are already in the playlist
        tracks_to_add = [release['track_uri'] for release in new_releases_sorted if release['track_uri'] not in existing_track_uris]

        if tracks_to_add:
            add_count = len(tracks_to_add)
            print(f"Adding {add_count} new {pluralize('track', add_count)} to the playlist '{PLAYLIST_NAME}'...")
            # Spotify API allows adding up to 100 tracks at a time
            for i in range(0, len(tracks_to_add), 100):
                batch = tracks_to_add[i:i+100]
                for attempt in range(MAX_RETRIES):
                    try:
                        sp.playlist_add_items(playlist['id'], batch, position=0)  # Insert at the top
                        break
                    except (SpotifyException, ReadTimeout, ConnectTimeout, ConnectionError) as e:
                        if attempt < MAX_RETRIES - 1:
                            time.sleep(RETRY_SLEEP)
                        else:
                            print(f"Failed to add tracks after multiple attempts: {e}")
            # Determine success message based on add_count
            if add_count == 1:
                print("Track added successfully.")
            else:
                print("Tracks added successfully.")
        else:
            print("No new tracks to add to the playlist.")

        # Save the updated history
        save_history(history)
        print(f"Saved tracked releases to '{HISTORY_FILE}'.")
        print("Update complete. Tracked releases and last run time have been saved.")

if __name__ == "__main__":
    main()

Instructions for the LLM:

    Understand the Requirements:
        The script should authenticate with Spotify using Spotipy.
        It needs to track new releases from the user's followed artists.
        It should update a playlist named "New Songs" with these new tracks without duplicating existing ones.
        Maintain a history of processed releases to ensure that the same releases aren't processed multiple times.

    Implement Utility Functions:
        Functions for removing articles from artist names, pluralizing words based on counts, and handling date formats.

    Handle Authentication:
        Use SpotifyOAuth with the required scopes.
        Securely manage client_id, client_secret, and redirect_uri.

    Manage Release History:
        Load and save processed releases from/to a JSON file.
        Initialize the history file if it doesn't exist.

    Fetch and Process Data:
        Retrieve followed artists.
        For each artist, fetch their latest releases.
        Filter out unwanted release types and older releases based on the first run.
        Collect new tracks from these releases.

    Manage Playlists:
        Check if the "New Songs" playlist exists; create it if not.
        Retrieve existing tracks in the playlist to prevent duplicates.
        Add new tracks in batches, handling API rate limits and potential errors.

    Implement Error Handling and Retries:
        Retry failed API requests up to a maximum number of attempts with delays between retries.
        Handle specific exceptions related to Spotipy and network issues.

    Provide User Feedback:
        Print informative messages to the console about the script's progress and any issues encountered.

    Ensure Code Quality:
        Write clean, readable, and well-documented code.
        Use descriptive naming conventions for functions and variables.
        Include docstrings for all functions.

Additional Notes:

    Environment Variables: Encourage the use of environment variables or a separate configuration file to store sensitive information (client_id, client_secret, etc.) instead of hardcoding them into the script.

    import os
    client_id = os.getenv('SPOTIFY_CLIENT_ID')
    client_secret = os.getenv('SPOTIFY_CLIENT_SECRET')
    redirect_uri = os.getenv('SPOTIFY_REDIRECT_URI')

    Caching Tokens: Spotipy handles token caching by default. Ensure that the cache file (.cache) is properly managed and secured.

    Deployment Considerations: If deploying the script to a server or as a scheduled task, ensure that the authentication flow is compatible with non-interactive environments.

Expected Deliverables:

    A Python script (spotify_release_tracker.py) that fulfills all the functional and technical requirements outlined above.
    Clear documentation or comments within the code explaining the purpose of functions and key sections.
    Instructions on how to set up and configure the script, including obtaining Spotify API credentials and setting environment variables.