Porting Playlists With Python

I had a brief sting last year where I was using Spotify, but I dumped it, mostly for financial reasons, but also because as much as I like the ability to just, listen to whatever, I kind of dislike the whole “Music as a service” aspect. I can still find new stuff via Youtube and then add it to my list of “Albums to maybe buy eventually”.

One thing I lost though was my Playlists. I was worried they were just gone, soon after logging in, I swear they hd just vanished, but checking now, they seem to all be there again. Whatever the case, I wanted a backup copy.

This is of course, an arduous thing to do, particularly my large “play random tracks” list, which has 1200+ songs. I don’t have time to type all that out, or to search and find all these tracks on Youtube. There are services, but they tend to be limited unless you want to pay, which is more annoying than anything.

Exporting from Spotify

Thankfully, i can use Python. I needed a script that would pull down my playlists and dump them to simplet text files. I actually had originally asked Perplexity to build this script, which it did, but the API method it used didn’t match the one I had previously used during my Python class, to make a Plylist generator for Spotify.

Instead of doing what would probably be the easier thing, and figuring out whate OAUTH method the Perplexity script uses, I just, rebuilt things using the Spotipy library, which is what I had used previously. So this script is one I made, for the most part.

It connects and gets a list of all the playlists you have, then loops through that list, and on ech playlist, pulls down all the track names, and writes them to a text file, in the format Artist – Album – Track Name.

The credentials go into a file int he same directory called auth,py with the following format of your Spotify Developer credentials.  Keep the quotation marks.

SPOTIPY_CLIENT_ID = "YOUR CLIENT ID"  
SPOTIPY_CLIENT_SECRET = "YOUR CLIENT SECRET"  
SPOTIPY_REDIRECT_URI = "http://localhost"  
SPOTIFY_USERNAME = "YOUR USER ID NUMBER"
import requests
import os
import spotipy
from auth import *
from spotipy.oauth2 import SpotifyOAuth

sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id=SPOTIPY_CLIENT_ID,
                                               client_secret=SPOTIPY_CLIENT_SECRET,
                                               redirect_uri=SPOTIPY_REDIRECT_URI,
                                               scope="user-library-read",
                                               cache_path="token.txt"))

def get_all_playlists():
    playlists = []
    limit = 50
    offset = 0
    playlists = sp.current_user_playlists(limit, offset)
    return playlists

## https://stackoverflow.com/questions/39086287/spotipy-how-to-read-more-than-100-tracks-from-a-playlist
def get_playlist_tracks(username,playlist_id):
    results = sp.user_playlist_tracks(username,playlist_id)
    tracks = results['items']
    while results['next']:
        results = sp.next(results)
        tracks.extend(results['items'])
    return tracks

def save_playlists_to_files(this_list, listname):
    if not os.path.exists('lists'):
        os.makedirs('lists')
    # Sanitize filename for filesystem
    safe_name = listname.replace('/', '_').replace('\\', '_')
    filename = f"lists/{safe_name}.txt"

    with open(filename, 'w', encoding='utf-8') as f:
         f.write(f"Playlist: {listname}\n")
         f.write("Tracks:\n")
            # Optionally, you can fetch and list track names here
         for eachtrack in this_list:
             f.write(f"{eachtrack}\n")

playlists = get_all_playlists()
#print(playlists)
for each in playlists['items']:
   this_list=[]
   #print(each['name'])
   listid = each['id']
   ownerid = each['owner']['id']
   #print("\n")
   mytracks = get_playlist_tracks(ownerid,listid)
   for eachtrack in mytracks:
      trackentry = f"{eachtrack['track']['artists'][0]['name']} - {eachtrack['track']['album']['name']} - {eachtrack['track']['name']}"
      this_list.append(trackentry)
      #print(trackentry)
   save_playlists_to_files(this_list, each['name'])

Everything gets output to a folder called “lists”.

Importing to Youtube

But what to do with these lists?  It’s going to be a bit more complicated to try to get Python to build them from my private music collection.  I have a LOT of the tracks, I don’t have all of the tracks, I also would need it to scan through well, it’s a fuckton, of music files, some tens of thousands, maybe more, decide on a file, and add it to a winamp or VLC playlist.

What I can do though, for now, is make a big ass YouTube Playlist.  

I have no experience with the Youtube API, so I just asked Perplexity for this script, specifically:

“create a python script that will take a text file list of sings, as an input, one song on each line, formatted “artist – Album – Song title” and search Youtube for the artist and song, and add the first result to a new playlist named after the name of the file”

It did some thinking, then gave me a script and instructions on how to set up OAUTH credentials on Youtube.  I then did a test run of the script on one of the shorter list files and, sure enough, it worked perfectly.  I have included the script below.

You need to create an app here, and create OATH Credentials, and download the file, place it int he folder with the script below, renammed to “client_secret.json”.

The script requires the following dependencies.

pip install google-auth-oauthlib google-auth-httplib2 google-api-python-client

Something not mentioned by Perplexity, that I found a solution for on Stack Overflow, after getting an error, you need to add users. On the App page (you should be sitting there after creating the app), Select the “Audience” tab on the side bar, then a bit down, add a “Test User” by email address, which is the email address associated with your Youtube Channel that you want ot attach the playlists.

import os
import argparse
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = ["https://www.googleapis.com/auth/youtube.force-ssl"]

def create_playlist_and_add_songs(file_path):
    # Authenticate and build service
    flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)
    credentials = flow.run_local_server(port=0)
    youtube = build("youtube", "v3", credentials=credentials)

    # Get playlist name from filename
    playlist_name = os.path.splitext(os.path.basename(file_path))[0]

    # Create new playlist
    playlist = youtube.playlists().insert(
        part="snippet,status",
        body={
            "snippet": {
                "title": playlist_name,
                "description": f"Auto-generated from {playlist_name}"
            },
            "status": {"privacyStatus": "private"}
        }
    ).execute()
    playlist_id = playlist["id"]

    # Process songs
    with open(file_path, "r") as f:
        for line in f:
            parts = line.strip().split(" - ", 2)
            if len(parts) != 3:
                print(f"Skipping malformed line: {line}")
                continue

            artist, album, song = parts
            query = f"{artist} {song}"
            
            # Search for video
            search_response = youtube.search().list(
                q=query,
                part="id",
                maxResults=1,
                type="video"
            ).execute()
            
            if not search_response.get("items"):
                print(f"No results for: {query}")
                continue
            
            video_id = search_response["items"][0]["id"]["videoId"]

            # Add to playlist
            youtube.playlistItems().insert(
                part="snippet",
                body={
                    "snippet": {
                        "playlistId": playlist_id,
                        "resourceId": {
                            "kind": "youtube#video",
                            "videoId": video_id
                        }
                    }
                }
            ).execute()
            print(f"Added {artist} - {song}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("input_file", help="Text file containing songs")
    args = parser.parse_args()
    create_playlist_and_add_songs(args.input_file)

And here is the final imported version of my Raffaella playlist from Spotify.

LinkedIn Daily Games

A lot of people like to play brain games, and while Crosswords have been popular for a while, Wordle really popularized the whole “Once Daily” craze for online games a few years ago. I have done Wordle, and a ton of variations on Wordle. I did Sudoku for a while. I was even doing Crosswords for a bit. Currently, my go to is the daily LinkedIn Games.

They are in the app, they may be on the web too, I don’t know, I don’t look at the website. Hell, I don’t even really look at the app except to play the 5 games they have.

Anyway, I wanted to talk a bit about these games for a bit, and what I think it means for my own thoughts processes. They more or less fall into two categories, word games, and special games. I am pretty good at all of them, but very good at the special games.

Crossclimb

Sort of a crossword like game, you have a series of words all of the same length, with clues for each word. But the trick is, the end result will be words that can be ordered in a way that only one letter changes.

So the one this morning had something like, “Ice turning to water” and “To get with another person” as clues.

Melt and Meet.

You have to arrange the words though, so one clue in between might be “To lose feathers”, Molt, which would come above.

I typically average half the average time on this puzzle daily. You get two hints, for a single letter or a full word, each has a cool down. It can be useful to just accept the idea of using these when younger really stuck, though I don’t use them often.

A better strategy is to just move on, then see where you have gaps in your arrangement to get clues.

Like the above examples, maybe you can’t get Melt, but you have Molt and Meet. Well, the M and T are common, so there is likely at least one word in between those, because two letters changed. It would take “two hops”. This also means you can conclude it’s either ME_T or M_LT or MO_T or M_ET. You basically have 4 words to try to sus out the answer from.

Pinpoint

Definitely weakest game, but probably not for being bad at it. You get 5 clues to find the common denominator. The first two tend to be really obscure, most of the time.its obvious by the 3rd, and the last word is usually very obvious.

My problem tends to be, I get too abstract with the connection and expect them to all be really obscure like the first clue sometimes is.

Today has Wet, Boiler, Bathing, Three Piece…

I guessed Slippery (when wet), Room (Wet Room, Boiler Room), Water (all are water things), Bathing Suit (which was wrong) but the answer was simply “Suit”.

It’s notable, that this is the only puzzle you can actual fail. All of the others just get a larger and larger timer, and as far as I can tell, the timer is unlimited.

Zip

This is a newer one, its kind of interesting, I am super good at it. Like I said, I am really great at the “special” puzzles.

You have a grid with a sequence of numbers laid across it, sometimes there are walls. Not every square has a number, it’s usually either 6 or 8 at the max. You have to start at 1, and connect the numbers in order, while also filling in all the squares.

You have to be able to get them in order, sometimes this means doing some zig zag to pick up blocks, also, you have to be able to see ahead a bit since you may need to leave a path to get a later number.

They feel pretty easy, but I find it fun to do and almost always get it in the 10-20 second range with 0-2 backtracks.

Tango

I want to like Tango more, but I am not sure what could really be changed to make it more enjoyable. The object is, you have a grid each row and column has 3 each of a sun or a moon. You can’t have more than two in a row of any symbol. There are sometimes indicators like = or x that means that two blocks must be equal or opposites.

Sometimes they replace the symbols for holidays. The final four used Basketball teal mogos, which honestly kind of fucked up my ability to logic, I think maybe the problem was the colors were sort of reverse from normal.

Every game of Tango feels like it’s either just, filling in a chain of obvious drops. Or, you make one guess in the right spot, usually one of the = or x nodes, and then fill in the logic chain until you hit a dead end or win. If you hit a dead end, clear the board, and start with the opposite.

Queens

Saving the best for last, I actually went looking for a place to JUST play Queens puzzles. I should try to program up something to generate them maybe.

It’s kind of a Sudoku style game.

For queens, you have a grid, it has several continuous colored sections or irregular shapes. They can be as small as one square. The object is to put a Queen crown in every colored section, but also, only one in each row and column

So you start with obvious singles. You also can eliminate some for anything that is a row.

The real trick to being quick as these is eliminating “large rows”. Say every color intersects the two middle rows. But two of the colors, exist entirely in the middle rows. You can eliminate two rows now from every other color except those two. Each row, has to be one of those two colors, because those colors can’t be placed anywhere else. These can be 2, 3,4 whatever high, though they become less useful if it’s 4 and are about useless if it’s 5, aside from maybe now you have 3 colors in the opposite set that makes a “large row”.

Another one is to catch where placing a crown eliminates a color completely, which can make choosing easy sometimes. If you have a 2 block nestles inside a 3 block “L”, you know it’s not going to be the “inside” box because you would remove all the spaces from the 3 block “L”. For example.

You run these “filters” across rows and columns and it makes a lot of the solutions become clear.

You do occasionally have to just guess and go for it. And after a while even when guessing, you might end up with no place to put a crown, but you have a good “feel” for how to adjust.

I usually win these very fast, within 20 seconds usually.

Sunday 2025-04-13 – Link List

Blogging Intensifies Link List for Sunday 2025-04-13

Record Store Day 2025

Another year, another Record Store Day. Though for my part it was Record Stores Day. This is only the second time I went to one of these, last year I don’t think there was anything I wanted. This year there was, this super cool 10th anniversary release of CHVRCHES Every Open Eye.

It’s getting a regular release later, but it won’t be the pretty “Clearest Blue” color. Like the song, on the album, possibly the best song in their catalog.

I am there in the middle in the green jacket. The store is around the corner and half a block down.

I went slightly better prepared this year. Instead of getting up early and driving over to Springfield, the nearest place hosting the RSD releases, I stayed over at my parents’ house and woke up early, and got in line. Last time I arrived at 8:30, a half hour after opening. This year was 6:30, one and a half hours before opening.

And like last time I just, chatted with others in line near me.

Eventually, I made it in and… Did not get my CHVRCHES album sadly. None left. I think there was probably only one at this location, because a lot of places only got two copies, and Dumb Records, where I went, has a second location that they split the albums across.

I know they had more than one, because Dumb Records posted a list of any albums only at a particular location.

I found a few others that seemed interesting but I also didn’t want to buy randomly.

It wasn’t a total loss of effort though, I had a second album I had wanted, which I did get. The Gorillaz Demon Days, Live at the Apollo Theater. Demon Days is a really great album, and love shows in general tend to be better than album versions.

I also got this sweet Dumb Records tote bag as a bonus, and I bought a Dumb Records Sticker, though I am not sure where I want to stick it. Maybe laptop, maybe I will slap it on my turn table, the plastic top is a little scuffed anyway.

One thing I did not get, because I forgot in my dismay over missing out on Every Open Eye, was some Dumb Records Coffee. I mean, it was basically just some other coffee from the local Arrowroot Coffee, branded for Dumb Records, but I asked the price, I said, “I will come get some after I look around” and then … I forgot.

Anyway, I left the Dumb Records and decided, I had time, I would take my chances and try Dumber Records across town for my CHVRCHES album. The Dumber Records is in the mall, which is not normally open at 9 o’clock in the morning. And there was no obvious entrance to use, so after circling the mall, a few times and trying one locked door, I found the right entrance. Unfortunately, no luck on the album I wanted there either.

I’m still good with the adventure.

A Brick of Coffee

Let’s do a “boring post”.

I bought a coffee brick. It’s not fancy, it came from Aldi. Hell, it’s possible this is the “least fancy” way to make coffee. Most of the time these days I buy beans and hand grind them, but I like to keep alternatives in case I am feeling lazy.

So I decided to try the “brick coffee”.

I was not sure what to expect here at all. Would I need to cut slices off and grind them up? Would I need some sort of other thing I didn’t have, how do I store it between uses?

So, as my kettle was warming, I set about opening it up. The packaging was a bit stubborn, and I ended up having to cut it open with some scissors and…

It’s… Basically just… Ground coffee… Shaped like a brick?

Kind of disappointing honestly. I wanted special tools to be involved.

There was a very satisfying “hisssss” when the package pressure was released, so it had that going for it.

The packaging suggested 1 tbsp per 6oz, and my French Press is roughly 32oz. I did some math and came up with “about 2/3rds cup of coffee. Which is more than I normally use, but I went for it anyway

Once the water was boiling, I poured some in and finished up with some other things around the kitchen while waiting the 5 minutes for it to steep.

For storage of the rest of the grounds, there was enough give in the package now to just use a regular bag clip.

The end result was, fairly strong, taste wise. Probably more because of how much coffee I had used. It was powerful enough to overpower my normal creamer flavor.

Overall though, I think I will stick to my coffee beans.