Friendship!!!

My friends and I have a colaboratory playlist called Dadesurs.

This script was created to show the contents and statistics of the playlist, and automatically send reminders to add songs to it.

Code

Setup

Library loading and custom displaying functions:

Code
# Spotpy and credential utilities:
from dotenv import load_dotenv
from os import getenv, listdir
import spotipy

# Data manipulation:
import polars as pl
from collections import Counter
import re
import random as rd
from emoji import EMOJI_DATA
import base64

# Email sending:
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

# Custom dataframe printing:
from IPython.display import display, Markdown
from itables import init_notebook_mode, show
init_notebook_mode(all_interactive=True)

def print_df(data, n = 5, as_md = True):
    config = pl.Config(
        tbl_hide_dataframe_shape = True,
        tbl_hide_column_data_types = True,
        tbl_formatting = 'ASCII_MARKDOWN' if as_md else 'UTF8',
        tbl_rows = n
    )

    with config:
        if as_md:
            display(Markdown(repr(data)))
        else:
            print(data)

    return None

Spotfy conection

Loading credentials from the enviroment variables and connecting to the Spotify API:

Code
load_dotenv()

auth = spotipy.oauth2.SpotifyOAuth(
    client_id = getenv("SPOTIPY_CLIENT_ID"),
    client_secret = getenv("SPOTIPY_CLIENT_SECRET"),
    redirect_uri = getenv("SPOTIPY_REDIRECT_URI"),
    scope = "user-library-read"
)

sp = spotipy.Spotify(auth_manager = auth)

Tracks’ data extraction

Extracting the tracks via the playlist ID, and extracting their data:

  • User names, customized via a lookup table.
  • Track names, with ” (feat. …)” and ” - Remastered …” type comments removed, via the "(.+) (\(.+\)|- .+)$" regex.
  • Artist (the main one) and popularity index.
  • Duration, converted to seconds.
  • Added date, removing the HH:MM:SS part with string indexing.
Code
tracks = sp.playlist_tracks("2dDtStk8Nk1PP0JxvdgPEK")["items"]

ids_dict = {
    "wh946a5syk0vfqe3538avp6xd": "Lau 🤠",
    "21m2c7t3edlhzjpcoifnnca6y": "Ric 😝",
    "arthur81u": "Atu 🥴"
}

data = pl.DataFrame({
    "User": [ids_dict[x] for x in [x["added_by"]["id"] for x in tracks]],
    "Track": [re.sub(r"(.+) (\(.+\)|- .+)$", r"\1", x["track"]["name"]) for x in tracks],
    "Artist": [x["track"]["artists"][0]["name"] for x in tracks],
    "Popularity": [x["track"]["popularity"] for x in tracks],
    "Duration": [x["track"]["duration_ms"] / 1000 for x in tracks],
    "Added Date": [x["added_at"][0:10] for x in tracks]
})

Data visualizations

For each user, getting aggregated stats, via Polars’ group_by + agg methods:

  • Number of tracks added.
  • Average popularity of the tracks.
  • Average duration, stylized into MmSS format.
Code
data_avg = data.group_by("User").agg([
    pl.col("User").count().alias("# Tracks"),
    pl.col("Popularity").mean().round(1).alias("Avg. Popularity"),
    pl.col("Duration").mean().alias("Avg. Duration")
]).with_columns(
    pl.col("Avg. Duration").map_elements(
        lambda s: f"{int(s//60)}m{int(s%60):02d}",
        return_dtype = str
    )
).sort(
    ("# Tracks", "User"), descending = (True, False)
)

Full data:

Code
data_full = data.with_columns(pl.col("Duration").map_elements(
    lambda s: f"{int(s//60)}m{int(s%60):02d}",
    return_dtype = str
)).sort(
    ("Added Date", "User")
)

Statistics

Average stats

Code
print_df(data_avg)
User # Tracks Avg. Popularity Avg. Duration
Atu 🥴 18 49.7 3m58
Ric 😝 18 49.8 3m40
Lau 🤠 15 50.0 3m07

Full table

Code
show(
    data_full,
    paging = False,
    scrollY = "500px",
    classes = "display nowrap compact table-striped small hover"
)
User Track Artist Popularity Duration Added Date
Loading ITables v2.2.4 from the init_notebook_mode cell... (need help?)

Reminders

Now, onto the automatic email reminders.

The first step is to randomize the subject line, and the content (a picture of us and a music-related quote).

Code
def get_random_color():
    return f"rgba({rd.randint(0, 256)}, {rd.randint(0, 256)}, {rd.randint(0, 256)}, 0.8)"

subject = rd.choice([
    'Ponha, ponha a musga imediatamente 🔪',
    'Cadê as músicas, seus vacilões? 🎶',
    'A playlist não vai se encher sozinha! 🚀',
    'Vamos lá, DJ! Solta o som! 🎧',
    'Sem música, sem festa! 🎉',
    'Não seja tímido, queremos te ouvir! 🎵',
    'A megacorporação Spotfy TM exige mais músicas! 🤖',
])

img_path = rd.choice(listdir('pictures/'))

with open('quotes.txt', 'r', encoding="utf-8") as file:
    quote = rd.choice([quote.strip() for quote in file.readlines()])

components = {
    "background-color": get_random_color(),
    "foreground-color": get_random_color(),
    "quote": quote
}

Then, these contents are injected in a HTML template, that has tags of the form "@@tag@@". The code is partially taken from the template_injector Python package that I created.

Code
with open('email_template.html', 'r', encoding="utf-8") as file:
    template = file.read()

# From https://github.com/ricardo-semiao/ricardo-semiao/tree/main/packages/template_injector
for key, value in components.items():
    pattern = re.compile('@@' + re.escape(key) + '@@')
    match = re.search(pattern, template)

    template = re.sub(f'@@{key}@@', value, template)

Lastly, the email is sent via SMTP. The user data is stored in the environment variables, and the content is build with MIME, specially the image attachment via CID.

Code
smtp_server = 'smtp.gmail.com'
smtp_port = 587
smtp_user = getenv("GMAIL_USER")
smtp_password = getenv("GMAIL_APP_PASSWORD")

from_email = getenv("GMAIL_USER")
receivers = [getenv("GMAIL_LAULA"), getenv("GMAIL_ATU")]

for to_email in receivers:
    msg = MIMEMultipart()
    msg['From'] = from_email
    msg['To'] = to_email
    msg['Subject'] = subject
    msg.attach(MIMEText(template, 'html'))

    with open(f'pictures/{img_path}', "rb") as file:
        img = MIMEImage(file.read())
        img.add_header('Content-ID', '<image1>')
        msg.attach(img)

    try:
        server = smtplib.SMTP(smtp_server, smtp_port)
        server.starttls()
        server.login(smtp_user, smtp_password)
        server.sendmail(from_email, to_email, msg.as_string())
        server.quit()
        print(f'Email sent successfully to {to_email}')
    except Exception as e:
        print(f'Failed to send email: {e}')

An example email is shown below: