Published 2020-03-01.
Last modified 2024-12-15.
Time to read: 2 minutes.
wpmc collection.
The Production Infrastructure Overview and Production Infrastructure Directories articles describe the environment that the scripts on this page operate within.
All of these scripts require Ruby to be set up; please see Setting Up a Ruby Development Environment.
Create Song Structure
The following script creates the subdirectory structure for a song. Any pre-existing files in the directory are moved to their appropriate subdirectories. A private GitHub repository is created for the song.
Use the A Streamlined Git Commit to ensure that only files that fit in Git are checked in.
Installation
-
Install dependencies:
Shell
$ gem install colorator
$ gem install mime-types -
Save the code as
song_createin a directory on thePATH, such as/usr/.local/ bin/ -
Make
song_createexecutable:
Shell$ sudo chmod a+x /usr/local/bin/song_create
Example Usage
This script can be called from any directory.
$ song_create $songs/test Creating a new song subdirectory structure in '/mnt/e/media/songs/test' /mnt/e/media/songs/test/daw /mnt/e/media/songs/test/dvrArchives /mnt/e/media/songs/test/inputs/image /mnt/e/media/songs/test/inputs/video /mnt/e/media/songs/test/outputs/audio /mnt/e/media/songs/test/outputs/stems /mnt/e/media/songs/test/outputs/youTube Opening E:\media\songs\test
Source Code
#!/usr/bin/env ruby
require 'colorator'
require 'fileutils'
require 'mime/types/full'
# https://github.com/mime-types/ruby-mime-types/
# gem install 'mime-types'
# Creates a new music video (song) subdirectory structure.
# Does not create any files, so this script is idempotent;
# you can accidently call it several times without problems.
#
# Creates this structure:
# ├── daw
# │ ├── live # Ableton Live sets
# │ └── pro_tools # Pro Tools sessions
# ├── dvrArchives # DaVinci Resolve project archives
# ├── inputs # For video editor (DaVinci Resolve) project
# │ ├── images # QR code, etc.
# │ └── videos # A and B rolls
# └── outputs # Renders, mostly by DAWs and video editor
# ├── mp3s # Pro Tools renders
# ├── stems # Zipped audio files
# └── youTube # DaVinci Resolve renders
#
# Sample output:
# $ song_create $songs/test
# The subdirectory structure '$songs/test' already exists. No problem!
# /mnt/e/media/songs/test/daw/live
# /mnt/e/media/songs/test/daw/pro_tools
# /mnt/e/media/songs/test/dvrArchives
# /mnt/e/media/songs/test/inputs/image
# /mnt/e/media/songs/test/inputs/video
# /mnt/e/media/songs/test/outputs/audio
# /mnt/e/media/songs/test/outputs/stems
# /mnt/e/media/songs/test/outputs/youTube
# Opening E:\media\songs\test
#
class SongStructure
attr_reader :this_song_dir, :song_name
def initialize(song_name)
unless song_name
puts "Error: No song name was provided. Aborting.".red
exit 1
end
@song_name = song_name
@songs_dir = ENV.fetch('songs') # root of all songs
unless @songs_dir
puts "Error: Environment variable 'songs' is not defined. Aborting.".red
exit 2
end
if @songs_dir.empty?
puts "Error: Environment variable 'songs' has no value. Aborting.".red
exit 3
end
unless Dir.exist? @songs_dir
puts "Error: Directory #{@songs_dir} does not exist. Aborting.".red
exit 4
end
@this_song_dir = "#{@songs_dir}/#{@song_name}" # root of the new song's directory
end
def create
if File.exist? @this_song_dir
puts "The subdirectory structure '#{@this_song_dir}' already exists. No problem!".yellow
else
puts "Creating a new song subdirectory structure in '#{@this_song_dir}'".yellow
end
[
'daw/live', 'daw/pro_tools',
'dvrArchives',
'inputs/audio', 'inputs/image', 'inputs/video',
'outputs/audio', 'outputs/stems', 'outputs/youTube',
].each do |dir|
puts " #{@this_song_dir}/#{dir}".yellow
FileUtils.mkdir_p "#{@this_song_dir}/#{dir}"
FileUtils.touch "#{@this_song_dir}/#{dir}/.gitkeep"
end
end
def display_this_song_dir
win_song_dir = `wslpath -w '#{@this_song_dir}'`.chomp
puts "Opening #{win_song_dir}".yellow
Dir.chdir("/mnt/c") do
`cmd.exe /c 'explorer.exe /separate, #{win_song_dir}'`
end
end
# Creates a git repo with the same name as the song, but with the 'song_' prefix added if required
def git_repo(this_song_dir, song_name)
Dir.chdir(this_song_dir) do
if Dir.exist? '.git'
puts "The song in '#{this_song_dir}' already has a git repository".yellow
return
else
`git init`
name = song_name.start_with?('song') ? song_name : "song_#{song_name}"
`gh repo create --private --source=. --remote=origin #{name}`
end
write_git_ignore
write_readme
`commit` # See https://mslinn.com/git/1050-commit.html
end
end
# Moves files into position based on:
# 1) Name
# 2) extension if no mimetype
# 3) mimetype (set by examining extension). The Mimetype gem only looks at the filetype,
# not the contents, so the file need not exist.
def move_files(song_dir)
Dir.chdir(song_dir) do
Dir.glob('*').each do |entry|
next if File.directory? entry
filetype = File.extname(entry)[1..]
mimetype = MIME::Types.type_for entry
media_types = mimetype.map(&:media_type).uniq
media_subtypes = mimetype.map(&:sub_type).uniq
subdirectory = if /README.*/.match?(entry) then next # Ignore this file
elsif /.*zip$/.match?(entry) then 'outputs/stems'
elsif media_types.include?('audio') && filetype != 'mp4'
if media_subtypes.include?('midi')
'midi' # Both an input and an output, so make it a peer of input/ and output/
elsif filetype == 'mp3' # This is probably an audio rendering
'outputs/audio'
elsif filetype.start_with? 'aif' # Might be a sample or a render
'outputs'
else # Sample assumed
'inputs/audio'
end
elsif media_types.include?('image') then 'inputs/image'
elsif media_types.include?('video') then 'inputs/video'
else # rubocop:disable Lint/DuplicateBranch
next # Leave unrecognized files where you find them
end
path = "#{@this_song_dir}/#{subdirectory}"
FileUtils.mkdir_p path
FileUtils.mv entry, "#{path}/#{entry}", verbose: true
end
end
end
# This method is not required any more, but I keeping it just in case
def rename_repo
remote_origin_url = `git config --get remote.origin.url`
repo_name = `basename -s .git #{remote_origin_url}`
return if repo_name.start_with?('song')
repo_names = `gh repo list --json name --jq .[].name`.chomp
renamed_repo = "song_#{@song_name}"
if repo_names.include? renamed_repo
puts "#{renamed_repo} already exists, not modified.".yellow
return
end
puts "Renaming git repo '#{@song_name}' to '#{renamed_repo}'.".yellow
`gh repo rename #{renamed_repo} --yes`
end
# Assumes the file should be written to the current directory
def write_file(name, contents)
if File.exist? name
puts "'#{@this_song_dir}/#{name}' already exists, so it was preserved.".yellow
else
puts "Writing '#{@this_song_dir}/#{name}'".yellow
File.open(name, 'w') do |f|
f << contents
end
end
end
# Assumes the file should be written to the current directory
def write_git_ignore
git_ignore = <<~HEREDOC
*.Identifier
*.tmp
*~
~*
.DS_Store
desktop.ini
WaveCache.wfm
HEREDOC
write_file '.gitignore', git_ignore
end
# Assumes the file should be written to the current directory
def write_readme
write_file 'README.md', <<~HEREDOC
# #{@song_name}
Created #{Time.now.strftime "%Y-%m-%d"}
HEREDOC
end
end
if __FILE__ == $PROGRAM_NAME
structure = SongStructure.new ARGV[0]
structure.create
structure.move_files structure.this_song_dir
structure.git_repo structure.this_song_dir, structure.song_name
structure.display_this_song_dir
end
Create Song Webpage
The following script creates a new web page on this website for a new song. Referring to the production overview image, it creates a new web page with the given name in the right-hand area pertaining to Jekyll.
A QR code is also created for the webpage URL, which begins with https://mslinn.com/songs/.
Installation
-
Install dependencies:
Shell
$ gem install colorator -
Save the code as
song_page_createin a directory on thePATH, such as/usr/.local/ bin/ -
Make
song_page_createexecutable:
Shell$ sudo chmod a+x /usr/local/bin/song_page_create
Example Usage
This script can be called from any directory.
It overwrites any previous .html file of the same name in
the collections/_songs/ subdirectory of the website.
Users should commit their changes before calling this script, just to be sure.
$ song_page_create my_song Wrote /var/sitesUbuntu/www.mslinn.com/collections/_songs/my_song.html
Source Code
#!/usr/bin/env ruby
require 'colorator'
require 'fileutils'
# Creates $msp/collections/_songs/#{song_name}.html from song_template.html in the same directory.
# Overwrites the file if present.
# Creates a QR code image.
#
# TODO: update template contents with qr code and other strings. Right now manual editing is necessary
#
# Sample output:
# $ song_page_create space_truckin
# Creating /var/sitesUbuntu/www.mslinn.com/space_truckin.html
# Opening the new web page in VSCode
#
class SongPageCreate
def initialize(song_name)
unless song_name
puts "Error: No song name was provided. Aborting.".red
exit 1
end
@song_name = song_name
@msp = ENV.fetch('msp') # root of mslinn.com (website)
unless @msp
puts "Error: Environment variable 'msp' is not defined. Aborting.".red
exit 2
end
if @msp.empty?
puts "Error: Environment variable 'songs' has no value. Aborting.".red
exit 3
end
unless Dir.exist? @msp
puts "Error: Directory #{@msp} does not exist. Aborting.".red
exit 4
end
@webpage_dir = "#{@msp}/collections/_songs" # path of the directory containing the web page for the new song
@template_path = "#{@webpage_dir}/song_template.html"
unless File.exist? @template_path
puts "Error: '#{@template_path}' does not exist. Aborting."
exit 1
end
@webpage_name = "#{@webpage_dir}/#{@song_name}.html" # path of the web page for the new song
@qr_image = "#{@webpage_dir}/images/qr_#{@song_name}.png"
@webpage_url = "https://www.mslinn.com/songs/#{@song_name}.html"
end
def create
if File.exist? @webpage_name
puts "The webpage '#{@webpage_name}' already exists. IT WILL BE NUKED AND REPLACED NOW.".yellow
else
puts "Creating a new song webpage in '#{@webpage_name}' from 'template.html' in the same directory.".yellow
end
FileUtils.cp @template_path, @webpage_name
end
def display_dir
webpage_name_win = `wslpath -w '#{@webpage_name}'`.chomp
puts "Opening the directory containing #{webpage_name_win}".yellow
`cmd.exe /c 'explorer.exe /separate, #{webpage_name_win}'`
end
# Overwrites
def qr_code
# yes | sudo apt install qrencode
`qrencode -o #{@qr_image} '#{@webpage_url}'`
end
end
if __FILE__ == $PROGRAM_NAME
spc = SongPageCreate.new(ARGV[0])
spc.create
spc.display_dir
spc.qr_code
end
Song Template
View the rendered song template for new songs, and examine its source code:
---
bpm: 120
history: 2024-11-28
date: 2024-11-28
key: C
description: "___, an original composition by Mike Slinn. All rights reserved."
exclude_from_outline: true
guitar_pro_file:
headerImage:
last_modified_at: 2025-04-09
layout: songs
live_file:
mp3:
musescore:
musicxml:
order: -1
pdf:
png:
pro_tools:
published: true
stems:
title: Song Template
---
<!-- #region about -->
{% capture about %}
<p>
This is the song template.
</p>
{% endcapture %}
<!-- endregion -->
<!-- #region audio -->
{%- comment -%}
{% capture audio %}
<p id="mp3_long">
</p>
{% include audio.html mp3="/songs/mp3s/space_truckin.mp3" %}
{% endcapture %}
{%- endcomment -%}
<!-- endregion -->
<!-- #region music_videos -->
{% comment %}
{% capture music_videos %}
{% include video.html youtube_id='6wMf1AHCJXE' %}
{% endcapture %}
{% endcomment %}
<!-- endregion -->
{% include song.html
about=about
audio=audio
change_log=change_log
competition=competition
how_to_play=how_to_play
learning_videos=learning_videos
lyrics=lyrics
music_videos=music_videos
planned=planned
tools_used=tools_used
%}
Zip Stems
The following script packages audio stems into a zip file. It performs the functionality denoted by the dashed line with the label zip in the production overview image.
Pro Tools generates audio stems with a common prefix. The zip file created by this script is automatically named from the common prefix of all the audio stem files.
Installation
-
Install dependencies:
Shell
$ gem install colorator
$ gem install rubyzip -
Save the code as
song_stemsin a directory on thePATH, such as/usr/.local/ bin/ -
Make
song_stemsexecutable:
Shell$ sudo chmod a+x /usr/local/bin/song_stems
Example Usage
This script can be called from any directory.
$ song_stems $songs/my_song Wrote /mnt/e/media/songs/my_song/outputs/stems/my_song_stems_120_bpm.zip
The following is identical to the above:
$ song_stems 'E:\media\songs\one_year_older\outputs\stems' Wrote /mnt/e/media/songs/my_song/outputs/stems/my_song_stems_120_bpm.zip
Source Code
#!/usr/bin/env ruby
require 'colorator'
require 'find'
require 'zip'
# gem install rubyzip
# gem 'rubyzip'
# Zips audio files in songs\your_song\outputs\stems into
# $msp/collections/_songs/stems/ and deletes the original files
#
# If this program is run from either of these song directories:
# /mnt/e/media/songs/one_year_older/
# /mnt/e/media/songs/one_year_older/outputs/stems/
# then the defaults will overwrite any zip file in
# the corresponding www.mslinn.com directory
# U:\var\sitesUbuntu\www.mslinn.com\collections\_songs\stems
#
# In other words, this invocation:
# $ cd $media/songs/one_year_older
# $ song_stems
# are the same as:
# $ song_stems E:/media/songs/one_year_older
# $ song_stems E:/media/songs/one_year_older U:/var/sitesUbuntu/www.mslinn.com/collections/_songs/stems
# $ song_stems $media/songs/one_year_older
# $ song_stems $media/songs/one_year_older $msp/collections/_songs/stems
#
# @param src [String] Can be either a Windows path or a WSL path
# @param dest [String] Defaults to common stems directory for all songs in mslinn.com
# Will be overwritten if it already exists.
class SongStems
def initialize(src = nil, dest = nil)
@src = src.nil? ? Dir.pwd : expand_env(src)
unless File.exist? @src # Did the user provide a WSL path?
# TODO: wslpath fails if @src does not exist; need to handle this
@src = `wslpath '#{@src}'`.chomp # Try again with a Windows path
unless File.exist? @src
puts "Error: directory '#{@src}' does not exist.".red
exit 1
end
end
@src += '/outputs/stems' if File.exist?("#{@src}/outputs/stems")
@dest = if dest.nil? || dest.empty?
"/var/sitesUbuntu/www.mslinn.com/collections/_songs/stems"
else
dest = expand_env dest
dest.exist? ? dest : `wslpath '#{dest}'`.chomp
end
unless File.exist? @dest
puts "Error: directory #{@dest} does not exist.".red
exit 2
end
@input_filenames =
Dir
.glob("#{@src}/*")
.select { |f| File.file?(f) && !f.end_with?('.zip') }
.map { |f| File.basename f }
if @input_filenames.empty?
puts "There are no audio stems in #{@src}, so this program has nothing to do.".red
exit
end
zipname = common_prefix(@input_filenames)
.gsub('_stem', '_stems')
.tr(' ', '_')
.delete_suffix('_')
@zip_path = "#{@dest}/#{zipname}.zip"
end
def expand_env(str)
str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
ENV.fetch(Regexp.last_match(1), nil)
end
end
def make
FileUtils.rm_f(@zip_path)
Zip::File.open(@zip_path, create: true) do |zipfile|
::Zip.sort_entries = true
@input_filenames.each do |filename|
zipfile.add(filename, File.join(@src, filename))
end
end
puts "Wrote #{@zip_path}".green
end
def delete_originals
@input_filenames.each { |f| File.delete "#{@src}/#{f}" }
end
def common_prefix(strings)
return '' if strings.empty?
result = 0
(0...strings.first.length).each do |k|
all_matched = true
character = strings.first[k]
strings.each { |str| all_matched &= (character == str[k]) }
break unless all_matched
result += 1
end
strings.first.slice(0, result)
end
end
if __FILE__ == $PROGRAM_NAME
stems = SongStems.new(ARGV[0])
stems.make
stems.delete_originals
end
Linked Mp3s and Images
I wrote about cross-file system linking options in NTFS/ext4 Compatible Aliases.
The following script performs the functionality denoted by the solid line with the label symlink in the production overview image. Step by step:
- Automatically categories any media file passed to it.
- Categorization is done on the basis of file type.
- Files can be accepted with syntax compatible with Bash, or using NTFS directory and file naming conventions.
- Incoming files are linked to their proper place.
Example Usage
$ cd $one_year_older $ song_link outputs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3 Wrote /var/sitesUbuntu/www.mslinn.com/songs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3
$ song_link $one_year_older/outputs/mp3s/One\ Year\ Older_55sec_136_bpm_2024-11-26.mp3 Writing /var/sitesUbuntu/www.mslinn.com/collections/_songs/mp3s/One Year Older_55sec_136_bpm_2024-11-26.mp3
Source Code
#!/usr/bin/env ruby
require 'clipboard'
require 'colorator'
# If this program is run from either of these song directories:
# $songs/one_year_older/
# $songs/one_year_older/outputs/mp3s/
# then by default any pre-exising media file in
# the proper subdirectory of $msp/collections/_songs/
#
# The following is true for two equivalent values of the environment variable called songs:
# export songs=/mnt/c/media/songs
# export songs='C:\media\songs'
# This invocation:
# $ cd $songs/one_year_older
# $ song_link outputs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3
# Performs the same action as all of the following:
# $ song_link $songs/one_year_older/outputs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3
# $ song_link $songs/one_year_older/outputs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3 $msp/collections/_songs/mp3s
# $ song_link C:/media/songs/one_year_older/outputs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3
# $ song_link C:/media/songs/one_year_older/outputs/mp3s/One_Year_Older_136_bpm_2024-11-02.mp3 U:/var/sitesUbuntu/www.mslinn.com/collections/_songs/mp3s
#
# @param src [String] Can be either a Windows path or a WSL path; relative or absolute
# @param dest [String] Defaults to common mp3s directory for all songs in mslinn.com
# Will be overwritten if it already exists.
class SongLinker
def initialize(src = nil, dest = nil)
@msp = compute_msp
@src = compute_src src
@src_name = File.basename @src
@song_name = compute_song_name @src
@dest_dir = compute_dest dest
@dest_path = "#{@dest_dir}/#{@src_name}"
end
def compute_dest(dest)
dest = expand_env(dest)
dest_www = if dest.nil? || dest.empty?
subdir = compute_subdir @src_name
"#{@wsl_vol}#{@msp}/collections/_songs/#{subdir}"
else
win_exist?(dest) ? dest : `wslpath -m '#{dest}'`.chomp
end
return dest_www if Dir.exist? dest_www
if File.exist? dest_www
puts "Error: Destination '#{dest_www}' is a file, not a directory. Aborting.".red
exit 1
end
puts "Error: Windows directory #{@dest_www} does not exist.".red
exit 2
end
def compute_msp
msp = ENV.fetch 'msp'
if msp.nil?
puts "Error: Environment variable msp is undefined".red
exit 3
end
return msp if Dir.exist? msp
if File.exist? msp
puts "Error: Environment variable msp points to a file, not a directory: '#{msp}'".red
else
puts "Error: Environment variable msp points to a directory that does not exist: '#{msp}'".red
end
exit 4
end
def compute_song_name(src)
result = src
.delete_prefix(expand_env("$songs/"))
.split('/')
.first
containing_dir = expand_env "$songs/#{result}"
unless File.exist? containing_dir
puts "Error: Song directory '#{containing_dir}' does not exist".red
exit 5
end
result
end
def compute_src(src)
src = Dir.pwd if src.nil?
src = File.realpath expand_env src
if src.empty?
puts "Error: src file to copy has no value. Aborting.".red
exit 6
end
unless File.exist? src # Did the user provide a WSL path?
# TODO: wslpath fails if src file to copy does not exist; need to handle this
src_tx = `wslpath '#{src}'`.chomp # Try again with a Windows path
unless File.exist? src_tx
puts "Error: file to copy '#{src}' does not exist.".red
exit 7
end
return File.realpath src_tx
end
File.realpath src
end
def compute_subdir(src_name)
case File.extname(src_name).delete_prefix('.')
when 'gp'
'guitar_pro'
when 'gif', 'jpg', 'jfif', 'png', 'webp'
'images'
when 'mp3', 'aiff'
'mp3s'
when 'mccz'
'musescore'
when 'xml'
'musicxml'
when 'pdf'
'pdfs'
when 'zip'
'stems'
end
end
# Does not work
def display_dir
win_home_dos = `cmd.exe /c "echo %systemdrive%%homepath%" 2> /dev/null`.chomp
win_home_linux = `wslpath -au '#{win_home_dos}'`.chomp
Dir.chdir win_home_linux do
@wsl_vol = wsl_volume_name
win_dir = unc_to_win(@dest_path)
puts "Opening #{win_dir}".yellow
`cmd.exe /c 'explorer.exe /separate, #{win_dir}'`
end
end
def expand_env(str)
return nil unless str
str.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
ENV.fetch(Regexp.last_match(1), nil)
end
end
def link
if File.exist? @dest_path
puts "Overwriting #{@dest_path}".green
File.delete @dest_path
else
puts "Writing #{@dest_path}".green
end
File.symlink @src, @dest_path
# Clipboard.copy(@dest_path)
end
def unc_to_win(path)
return path.delete_prefix(@wsl_vol) if path.start_with? @wsl_vol
return path.delete_prefix('//wsl$/Ubuntu') if path.start_with? '//wsl$/Ubuntu'
path.delete_prefix('//wsl.localhost/Ubuntu')
end
def wsl_volume_name
line = `cmd.exe /c net.exe use`
.split("\n")
.find { |x| x.include? 'Plan 9 Network Provider' }
if line.nil? || line.empty?
puts 'WSL has no drive letter mapped. Aborting.'
exit 1
end
line.strip.split.first
end
end
if __FILE__ == $PROGRAM_NAME
linker = SongLinker.new(ARGV[0])
linker.link
# linker.display_dir
end