In [ ]:
# Once upon a time I got interested in Magic the Gathering and remembered that one of my favorite youtubers, 
#  Day[9], had a show about MtG on Geek and Sundry.
# I decided to use my python skills and write a little utility to format youtube playlists for the
#  desktop player I use to watch Youtube videos.
In [ ]:
# First you set up application access permissions on your Google account if you want to use Google APIs.
# That grants you 'client_secret.json', a file with a few variables you need to get an access token later.
In [27]:
# Our imports:
# interface for google APIs
import google.oauth2.credentials
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow

import regex   # find the coveted string
import json    # save and load access information
import isodate # format video durations
In [28]:
# Settings for the script
CLIENT_SECRETS_FILE = "client_secret.json"
SCOPES = ['https://www.googleapis.com/auth/youtube.readonly']
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
In [ ]:
# You need to request an authorization code to get an access and refresh token,
#  which you can then save and use pretty much forever.

flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES)
credentials = flow.run_console()
In [ ]:
# You are given a url where you authorize the application with a Google account, and you receive the code.
# (I didn't explicitly do it here, since the url itself reveals some security-sensitive account information)
"Please visit this URL to authorize this application: "
"Enter the authorization code: "
In [ ]:
# Upon completing that, we get a credentials variable, and we can cherry-pick the most important components.
creds = {
    'access_token'  : credentials.token,
    'refresh_token' : credentials.refresh_token,
    'token_uri'     : credentials.token_uri,
    'client_id'     : credentials.client_id,
    'client_secret' : credentials.client_secret
}
with open('creds.json', 'w+') as fout :
    json.dump(creds, fout)
    
# Until a few days ago, there were API functions to save credentials data, now we have to do it by hand.
In [29]:
# And this is what usage looks like now, load the file and plot it into the Credentials constructor:
with open('creds.json', 'r') as fin :
    creds = json.load(fin)

credentials = google.oauth2.credentials.Credentials(
creds['access_token'],
refresh_token = creds['refresh_token'],
token_uri     = creds['token_uri'],
client_id     = creds['client_id'],
client_secret = creds['client_secret'])

# And this is our instrument of war. We can now run queries against Youtube Data API.
service = build(API_SERVICE_NAME, API_VERSION, credentials = credentials)
In [30]:
# Test query, and we'll save the channel ID for later:
results = service.channels().list(part='snippet,contentDetails,statistics',
                                  forUsername='geekandsundry').execute()
print('This channel\'s ID is %s. Its title is %s, and it has %s views.' %
        (results['items'][0]['id'],
         results['items'][0]['snippet']['title'],
         results['items'][0]['statistics']['viewCount']))
channel_id = results['items'][0]['id']
This channel's ID is UCaBf1a-dpIsw8OxqH4ki2Kg. Its title is Geek & Sundry, and it has 493066662 views.
In [ ]:
# Everything's working, now we get to the interesting parts.

# This is the playlist format of the media player:
#  i is integer position, starting with 1
MPCPLAYLIST # first line
"i",type,0
"i",label,"video title"
"i",time,"duration HH:MM:SS"
"i",filename,"video url"
In [31]:
# First we gotta find the playlist we want to convert.

playlist_id = ''
playlist_itemcount = 0
playlist_title = ''

# If we spot Day[9] in the title - that's the one.
re = regex.compile(r'Day\[9\]')

# Here's how we gather all the playlists:
pl = service.playlists().list(part='id, contentDetails, snippet',
                              channelId=channel_id).execute()
counter = 0
while (True) :
    for i in pl['items'] :
        counter += 1
        
        title = i['snippet']['title']
        id    = i['id']
        count = i['contentDetails']['itemCount']
        print(title)
        print(id)
                
        if (re.search(title)) :
            print('GOT IT!!')
            playlist_id = id
            playlist_itemcount = count
            playlist_title = title            
        
        print('')
    
    # Youtube Data API retrieves items in pages of 5, and we need to make continious queries and "next page" tokens.
    if(pl.get('nextPageToken')) :
        pl = service.playlists().list(part='id, contentDetails, snippet',
                                      channelId=channel_id,
                                      pageToken=pl['nextPageToken']).execute()
    else :
        break

print(str(counter) + ' playlists total')
Callisto 6
PL7atuZxmT957wz-a95ESEGW7de6SgR9is

Relics and Rarities
PL7atuZxmT954B-AtfJsw_YbULHL_L7J8z

Overlight: Fractured Paradox
PL7atuZxmT955U2g4kVh-NmlyBjc2Zm-Vi

Game the Game: Betrayal Legacy
PL7atuZxmT955-Uk62HDtl-sgAOR5_zyxP

Talks Machina
PL7atuZxmT954R5TjnUcGSHDhUJuvytmtD

Starter Kit: D&D Edition
PL7atuZxmT955yt0W4QP5VD4TwLQvV3EiM

Vampire: The Masquerade - L.A. By Night
PL7atuZxmT957CplbNHCN5JAGp9SoZhyUH

Orbital Redux
PL7atuZxmT957mkD_WLOZcL6fHuMR2RGXI

Game Engine
PL7atuZxmT95795RrYsSUSq_Kkpjc5Xk8E

How to Play Magic: The Gathering
PL7atuZxmT954yIQUUjj8veJ9Vl1gP7wu_

International Tabletop Day 2018
PL7atuZxmT955DYbWE3JVJGLYjIYBmExd4

Weave Society
PL7atuZxmT957IzaSoNLyd2dImzOFR2iHh

Critical Recap
PL7atuZxmT957HmF7l0AFBLyC4w8TTvVTd

G&S Painters Guild
PL7atuZxmT955o2dShsr4403qxeE7y2d-t

Dick & Johnson
PL7atuZxmT956wriVDyMe1nJxJHFnNJqUj

Critical Role | Campaign 2
PL7atuZxmT955Cw-fFS-_3IQvaCpQgDzWA

Game the Game
PL7atuZxmT954kvCRXxi8klz8_RSftIB4I

Adulting in the Apocalypse w/ Whitney Moore
PL7atuZxmT957Emjl41iH7xErgLdL1EFtA

Wednesday Club One-Shots!
PL7atuZxmT956XHAEMoYDhUVEmlt3jTCeK

Shield of Tomorrow
PL7atuZxmT955aQO-G-3LR6kgX4EU2qrHg

International Tabletop Day 2017
PL7atuZxmT957_P7cUJIZKI4mo2ghyK8MA

Mothership
PL7atuZxmT955EzUt-1gj2RxBdzfM3r5OH

ForeverVerse w/ Ivan Van Norman
PL7atuZxmT956nOe8Cm6QoRSkT3YiRpT6k

Escape! With Janet Varney
PL7atuZxmT957_N4O2Jp3bMXmq_Y7J-mK-

TableTop: Season 4
PL7atuZxmT955LMr27MkQzrV2adQtU3o5U

Worthy Opponents: Hearthstone
PL7atuZxmT954Pe1EHJg0xquiFBI3BeP6R

Vast
PL7atuZxmT956Z8tNqlrZRhXH-i_oMfMWV

How to Play
PL7atuZxmT955l2-Gm0BKoL_I19WSULv9n

Anime Gateways
PL7atuZxmT957cdGNPe5ggm701RUqDxE-4

Open Beta Week 2016
PL7atuZxmT954eOJ_3XrVxnJEdql8ygsAO

Signal Boost
PL7atuZxmT954xvKazv4eg4ZiWvAo0HAbS

Riftworld Chronicles
PL7atuZxmT9569HMDiW3pNKiztxs-ErcAk

GM Tips
PL7atuZxmT9570U87GhK_20NcbxM43vkom

Omnibus
PL7atuZxmT9564wfWBjAYo8UEz8U4RxivI

Critical Role | Campaign 1
PL7atuZxmT954bCkC062rKwXTvJtcqFB8i

Titansgrave
PL7atuZxmT954UvydqNVClXAH3GRT0pAp2

Twitch
PL7atuZxmT957x3Bxh0vT22TyK1Eo5RBo9

LARPs: The Series
PL7atuZxmT95508O3ylmNMe_DOG6ksPdFn

TableTop Bonus Videos
PL7atuZxmT956RY4N7dMdKsPQSGPXs3_4x

Morganville: The Series
PL7atuZxmT954TvdqgzNfYdQ-btKcAKvWU

Guardians of the Galaxy
PL7atuZxmT954CBmgwgQPAf-rH2uU3Aiti

Wil Wheaton's TableTop
PL7atuZxmT956cWFGxqSyRdn6GWhBxiAwE

Spellslingers hosted by Day[9]
PL7atuZxmT954tuse5BmPzrVwPDogUKaxi
GOT IT!!

Player Piano
PL7atuZxmT956TuJ842a5A9ktdGeo2KqU-

Spooked Bonus
PL7atuZxmT9556q3zeycxadAzsuoPMfDif

Arcade Arms - Hosted by Nika Harper
PL7atuZxmT956UDizukeL9TLpOZGFQBTf-

Spooked - A Paranormal Comedy
PL7atuZxmT955yE91jpq4TGzmf5ZrqVhTG

Caper
PL7atuZxmT955AB7VSyUjSrIfbWLcamstn

Caper Bonus
PL7atuZxmT956iMG7I2cQcT8-UShBeftWC

Spellslingers Gag Reel
PL7atuZxmT957DPrlUHmkzTSRQpoA09tKU

GastroGeek
PL7atuZxmT956kzXYyO39w-N6bHmmw3saz

NOT the Flog
PL7atuZxmT956ze3m-GbR5Hit85oz1Zpz9

Co-Optitude
PL7atuZxmT956QzFpiCWAQT0uAOUU2iNcr

Space Janitors Season 2
PL7atuZxmT955ckM9ZOdQpe6oIHvdV8R48

Felicia's Ark
PL7atuZxmT957ndtR5U6Cc8q80WlrBJRjR

LearningTown Music Videos
PL7atuZxmT957xahYgEL2OqllJu3_om-O3

The Flog Music Videos
PL7atuZxmT957CLNZC7GULzqUlNgVZYuvu

Written By A Kid Bonus Videos
PL7atuZxmT954vc4c-kS6QjCdz4Bs-RbY5

Space Janitors Bonus Videos
PL7atuZxmT9576PaN99KqtPJ1rLLmoXcSv

LearningTown
PL7atuZxmT957CmcVYWWkiAUUnAl8h4BnJ

Vaginal Fantasy
PL7atuZxmT957Ihj2bDXAQhFVbe0SN-cY3

Space Janitors Season 1
PL7atuZxmT954qKicLcYtpk20SRDZaKCs2

Written by a Kid
PL46BCD70F1C029AD1

The Flog
PL8D3EFFBB747B9769

Dark Horse Motion Comics
PLD63319182D9529A4

65 playlists total
In [32]:
# Got it!
print(playlist_title)
print(playlist_id)
print('Has ' + str(playlist_itemcount) + ' videos')
Spellslingers hosted by Day[9]
PL7atuZxmT954tuse5BmPzrVwPDogUKaxi
Has 65 videos
In [58]:
# Now, in a similar fashion, we'll go through the playlist items, gathering required parameters:

id_prefix = 'https://www.youtube.com/watch?v='

playlist_info = []
playlist_itemlist = service.playlistItems().list(part='contentDetails, id, snippet',
                                                 playlistId=playlist_id).execute()
playlist_length   = playlist_itemlist['pageInfo']['totalResults']

while(True) :
    for i in playlist_itemlist['items'] :
        title       = i['snippet']['title']
        vid_id      = i['contentDetails']['videoId']
        dur         = service.videos().list(part='contentDetails',
                                           id=vid_id).execute()
        dur_str     = isodate.parse_duration(dur['items'][0]['contentDetails']['duration'])
        dur_format  = '%H:%M:%S'
        dur_str     = str(isodate.time_isoformat(dur_str, dur_format))
        
        vid_id      = id_prefix + vid_id
        
        #playlist_info.append((title, vid_id, dur_str, position, published))
        playlist_info.append((title, vid_id, dur_str))
        
    if(playlist_itemlist.get('nextPageToken')) :
        playlist_itemlist = service.playlistItems().list(part='contentDetails, id, snippet',
                                                         playlistId=playlist_id,
                                                         pageToken=playlist_itemlist['nextPageToken']).execute()
    else :
        break
In [59]:
# Let's see what it looks like:
playlist_info[:5]
Out[59]:
[('Gag Reel! Day[9] vs. Kari Byron | Magic: The Gathering: Spellslingers',
  'https://www.youtube.com/watch?v=EP4-kp8p_7Q',
  '00:02:15'),
 ('Day[9] vs. Kari Byron | Magic: The Gathering: Spellslingers | Season 5, Episode 6',
  'https://www.youtube.com/watch?v=_5av6N0JYhk',
  '00:25:48'),
 ('Gag Reel! Day[9] vs. Allison Scagliotti | Magic: The Gathering: Spellslingers',
  'https://www.youtube.com/watch?v=ogYc31pU23c',
  '00:03:10'),
 ('Day[9] vs. Allison Scagliotti | Magic: The Gathering: Spellslingers | Season 5, Episode 5',
  'https://www.youtube.com/watch?v=UbRi-yeKcvo',
  '00:26:26'),
 ('Gag Reel! Day[9] vs. Joe Manganiello | Magic: The Gathering: Spellslingers',
  'https://www.youtube.com/watch?v=MkVSirC1cNc',
  '00:03:39')]
In [56]:
# Looks like the playlist, as it the often the case with youtube playlists, is reversed.
playlist_info.reverse()

# We got everything we need. Time to format.
filename = 'Spellslingers.mpcpl'
counter = 0
with open(filename, 'w') as f :
    f.write('MPCPLAYLIST\n')
    for info in playlist_info :
        counter += 1
        title, url, dur = info
        f.write('%d,type,0\n'      % counter)
        f.write('%d,label,%s\n'    %(counter, title))
        f.write('%d,time,%s\n'     %(counter, dur))
        f.write('%d,filename,%s\n' %(counter, url))
In [62]:
# And there it is.
with open(filename, 'r') as f:
    lines = f.readlines()
for line in lines[:13]:
    print(line)
for line in lines[-12:]:
    print(line)
MPCPLAYLIST

1,type,0

1,label,Day[9] vs. Rob Simpson in Magic: The Gathering: Spellslingers Episode 1

1,time,00:21:13

1,filename,https://www.youtube.com/watch?v=KmouDgc9hD8

2,type,0

2,label,Day[9], Rob Simpson, Spellslingers Gag Reel - Eat Your Foes as Popcorn

2,time,00:03:05

2,filename,https://www.youtube.com/watch?v=C9J1x35QPZg

3,type,0

3,label,Day[9] vs. Bobak Ferdowsi in Magic: The Gathering: Spellslingers Ep 2

3,time,00:18:05

3,filename,https://www.youtube.com/watch?v=U-rOGGiJTQY

63,type,0

63,label,Gag Reel! Day[9] vs. Allison Scagliotti | Magic: The Gathering: Spellslingers

63,time,00:03:10

63,filename,https://www.youtube.com/watch?v=ogYc31pU23c

64,type,0

64,label,Day[9] vs. Kari Byron | Magic: The Gathering: Spellslingers | Season 5, Episode 6

64,time,00:25:48

64,filename,https://www.youtube.com/watch?v=_5av6N0JYhk

65,type,0

65,label,Gag Reel! Day[9] vs. Kari Byron | Magic: The Gathering: Spellslingers

65,time,00:02:15

65,filename,https://www.youtube.com/watch?v=EP4-kp8p_7Q