Skip to content

Instantly share code, notes, and snippets.

@dusekdan
Created May 26, 2018 18:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dusekdan/5e9179a5fa5a63e14211d13a5d26e120 to your computer and use it in GitHub Desktop.
Save dusekdan/5e9179a5fa5a63e14211d13a5d26e120 to your computer and use it in GitHub Desktop.
import sys
import os
import datetime
from shutil import copy
# By far the easiest way to get song duration is to use mutagen library. You
# can install it using PIP as follows:
# pip install mutagen
from mutagen.mp3 import MP3
from mutagen import MutagenError
from random import randint
# ____ ___ _ _ _____ ___ ____
# / ___/ _ \| \ | | ___|_ _/ ___|
# | | | | | | \| | |_ | | | _
# | |__| |_| | |\ | _| | | |_| |
# \____\___/|_| \_|_| |___\____|
#
# Written by: Daniel Dusek (@DusekDan)
# When there is less than SKIP_REMAINING_DURATION seconds left till the end of
# constructed playlist part, no additional songs will be added. 60 seconds
# proved itself to be a reasonable value.
SKIP_REMAINING_DURATION = 60
# Directory (relative to this script) to which MP3 songs should be copied when
# playlist is prepared.
OUTPUT_PLAYLIST_DIRECTORY = "GeneratedPlaylist2"
# Workout phases' duration in minutes.
PREWORKOUT_DURATION = 20 # Way to gym. Motivation recommended.
CARDIO_DURATION = 10 # Warm up: 10 minutes, that should do.
WORKOUT_DURATION = 30 # Kill it for 30 minutes straight.
KEEP_GOING_DURATION = 20 # For when lifting becomes too hard.
# Source folders with MP3 music files for 4 significant workout parts.
# Your directory structure should look something like this:
# ./
# |-> PlaylistPreparer.py
# |-> 00_preworkout/ (contains mp3 files with your preworkout songs)
# |-> 01_cardio/ (the same as above)
# |-> ... (folders of other workout parts)
# |-> GeneratedPlaylist/ (playlist files will be generated here)
PRE_WORKOUT_FOLDER = "00_preworkout"
CARDIO_FOLDER = "01_cardio"
WORKOUT_FOLDER = "02_workout"
KEEP_GOING_FOLDER = "03_keepGoing"
# Piece of advice: Preworkout & Keep-going should overlap, as if you keep-going
# long enough, the preworkout may start playling (loop).
standard_duration = (PREWORKOUT_DURATION * 60, CARDIO_DURATION * 60, WORKOUT_DURATION * 60, KEEP_GOING_DURATION * 60)
def main():
# Song files & song files info extraction.
preworkoutSongs = extractNameDurationTuples(PRE_WORKOUT_FOLDER)
cardioSongs = extractNameDurationTuples(CARDIO_FOLDER)
workoutSongs = extractNameDurationTuples(WORKOUT_FOLDER)
keepgoingSongs = extractNameDurationTuples(KEEP_GOING_FOLDER)
# Partial playlists preparation.
preworkoutPlaylist = preparePlaylist(preworkoutSongs, standard_duration[0])
cardioPlaylist = preparePlaylist(cardioSongs, standard_duration[1])
workoutPlaylist = preparePlaylist(workoutSongs, standard_duration[2])
keepgoingPlaylist = preparePlaylist(keepgoingSongs, standard_duration[3])
# Merging partial playlists into single final one.
finalPlaylist = preworkoutPlaylist + cardioPlaylist + workoutPlaylist + keepgoingPlaylist
# Flip the condition if you (don't) want to see songs used in playlist.
if (False):
peakPlaylist(finalPlaylist)
# Anonymize & move actual songs to playlist directory.
anonymizePlaylistSongs(finalPlaylist, OUTPUT_PLAYLIST_DIRECTORY)
print("Playlist created. Total time: " + secondsToMinutesString(getPlaylistDuration(finalPlaylist)))
def extractNameDurationTuples(targetFolder):
"""Iterates over target folder and retrieves tuples where key is a path to
song file and the value is its duration in seconds."""
songTuples = []
for file in os.listdir(targetFolder):
try:
audio = MP3(targetFolder + '/' + file)
duration = audio.info.length
songTuples.append((targetFolder + '/' + file, duration))
except MutagenError:
print("Unable to load information for file " + file + ". This file will be excluded from playlist generation.")
return songTuples
def preparePlaylist(songsource, duration):
"""From song-list of tuples (path_to_song_file, duration_in_seconds)
composes playlist of requested length (in seconds)."""
playlist = []
while getPlaylistDuration(playlist) <= duration:
remainingDurationCap = duration - getPlaylistDuration(playlist)
# Avoid filling up playlist with 'shortest possible' songs if there is
# less than SKIP_REMAINIG_DURATION seconds remaining to the requested
# duration.
if remainingDurationCap < SKIP_REMAINING_DURATION:
break
playlist.append(getRandomSongFromSongListWithDuration(songsource, remainingDurationCap))
return playlist
def getRandomSongFromSongListWithDuration(songlist, duration):
# Retrieve only songs with required duration.
requiredDuration = [s for s in songlist if s[1] <= duration]
# If there is no song satisfying duration requirements, the shortest one
# is provided instead.
if len(requiredDuration) == 0:
return findShortestSong(songlist)
# In case of multiple songs with requested duration, pick randomly.
index = randint(0, len(requiredDuration)-1)
return requiredDuration[index]
def anonymizePlaylistSongs(playlist, copyTo, external=""):
"""Rename & move playlist songs to the final destination. Renamed songs
respect their original order, but user can not tell which song was actually
added to the playlist. If external parameter is set, files will be copied
to copyTo location prefixed by external_root parameter."""
# Ensure external/copyTo directory exists
if not os.path.exists(external + copyTo):
os.makedirs(external + copyTo)
pi = 0 ; ci = 0 ; wi = 0 ; ki = 0
for song in playlist:
# Anonymize based on source (and optionally prefix with external path)
if song[0].startswith(PRE_WORKOUT_FOLDER):
newPath = external + copyTo + "/AAA_" + str(pi) + ".mp3"
pi += 1
elif song[0].startswith(CARDIO_FOLDER):
newPath = external + copyTo + "/BBB_" + str(ci) + ".mp3"
ci += 1
elif song[0].startswith(WORKOUT_FOLDER):
newPath = external + copyTo + "/CCC_" + str(wi) + ".mp3"
wi += 1
else:
newPath = external + copyTo + "/DDD_" + str(ki) + ".mp3"
ki += 1
copy(song[0], newPath)
# Strip ID3 tags to make all MP3s equal (and sorted only by name).
eraseID3Tags(external, copyTo)
def eraseID3Tags(root, target):
"""Loops over files copied to target directory and erases ID3 Tags."""
for file in os.listdir(root + target):
if file.lower().endswith('.mp3'):
try:
mp3 = MP3(root + target + "/" + file)
mp3.delete()
mp3.save()
except MutagenError:
print("Failed to erase ID3 tags for file %s" % file)
def getPlaylistDuration(playlist):
"""Calculates playlist duration in seconds."""
duration = 0
for record in playlist:
duration += record[1]
return duration
def findShortestSong(songlist):
"""Very ugly implementation of finding minimum value in a list of tuples."""
shortestDuration = sys.maxsize
shortestSongName = "NO-SHORTEST-SONG (Empty song list)"
for song in songlist:
# Discover local minimum.
if song[1] < shortestDuration:
shortestSongName = song[0]
shortestDuration = song[1]
return [t for t in songlist if t[0] == shortestSongName][0]
def secondsToMinutesString(seconds):
"""Handy function to convert duration in seconds to printable string."""
return str(datetime.timedelta(seconds=seconds))
def peakPlaylist(playlist):
"""In case you do not like surprise playlists, just switch the if (False)
condition to if (True) in main function."""
print("Playlist before anonymization:")
for song in playlist:
print("\t%s (%s)" % (song[0], secondsToMinutesString(song[1])))
# Non-import entry point.
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment