« get me outta code hell

http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--bill-everything.json754
-rw-r--r--bill-instrumentals.json182
-rw-r--r--play.js457
-rw-r--r--src/loop-play.js69
-rw-r--r--src/pickers.js33
-rw-r--r--src/play.js159
-rw-r--r--src/playlist-utils.js109
-rw-r--r--src/process-argv.js30
-rw-r--r--src/promisify-process.js19
-rw-r--r--todo.txt65
11 files changed, 485 insertions, 1393 deletions
diff --git a/.gitignore b/.gitignore
index a1a06bd..a165968 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 /.*.wav
 /.temp-track
+.DS_Store
 node_modules
 playlist.json
diff --git a/bill-everything.json b/bill-everything.json
deleted file mode 100644
index bdf115b..0000000
--- a/bill-everything.json
+++ /dev/null
@@ -1,754 +0,0 @@
-[
-  [
-    "christnuts, new zealand",
-    "http://www.billwurtz.com/christnuts-newzealand.mp3"
-  ],
-  [
-    "i'm gonna do the things that i gotta do",
-    "http://www.billwurtz.com/im-gonna-do-the-things-that-i-gotta-do.mp3"
-  ],
-  [
-    "_____notbugs",
-    "http://www.billwurtz.com/_____notbugs.mp3"
-  ],
-  [
-    "the road",
-    "http://www.billwurtz.com/the-road.mp3"
-  ],
-  [
-    "this is not a song",
-    "http://www.billwurtz.com/this-is-not-a-song.mp3"
-  ],
-  [
-    "i can play",
-    "http://www.billwurtz.com/i-can-play.mp3"
-  ],
-  [
-    "it's ok to love",
-    "http://www.billwurtz.com/its-ok-to-love.mp3"
-  ],
-  [
-    "i hope we figure it out",
-    "http://www.billwurtz.com/i-hope-we-figure-it-out.mp3"
-  ],
-  [
-    "all u gotta do is",
-    "http://www.billwurtz.com/all-u-gotta-do-is.mp3"
-  ],
-  [
-    "i wanna sail you away",
-    "http://www.billwurtz.com/i-wanna-sail-you-away.mp3"
-  ],
-  [
-    "hey jodie foster",
-    "http://www.billwurtz.com/hey-jodie-foster.mp3"
-  ],
-  [
-    "goo soup",
-    "http://www.billwurtz.com/goo-soup.mp3"
-  ],
-  [
-    "brooklyn museum",
-    "http://www.billwurtz.com/brooklyn-museum.mp3"
-  ],
-  [
-    "the looking glass",
-    "http://www.billwurtz.com/the-looking-glass.mp3"
-  ],
-  [
-    "i'm gonna plan my day",
-    "http://www.billwurtz.com/i%27m-gonna-plan-my-day.mp3"
-  ],
-  [
-    "this is a song for my next album",
-    "http://www.billwurtz.com/this-is-a-song-for-my-next-album.mp3"
-  ],
-  [
-    "i'm confused (i love you)",
-    "http://www.billwurtz.com/imconfused.mp3"
-  ],
-  [
-    "write a song on the count of 3",
-    "http://www.billwurtz.com/write-a-song-on-the-count-of-3.mp3"
-  ],
-  [
-    "i like to sleep around",
-    "http://www.billwurtz.com/i-like-to-sleep-around.mp3"
-  ],
-  [
-    "goodbye",
-    "http://www.billwurtz.com/goodbye.mp3"
-  ],
-  [
-    "macy's",
-    "http://www.billwurtz.com/macys.mp3"
-  ],
-  [
-    "no brains",
-    "http://www.billwurtz.com/no-brains.mp3"
-  ],
-  [
-    "It's Gonna Be Alright",
-    "http://www.billwurtz.com/itsgonnabealright.mp3"
-  ],
-  [
-    "new canaan",
-    "http://www.billwurtz.com/newcanaan.mp3"
-  ],
-  [
-    "the 'ngiueh' song",
-    "http://www.billwurtz.com/ngiueh.mp3"
-  ],
-  [
-    "drink beans",
-    "http://www.billwurtz.com/drinkbeans.mp3"
-  ],
-  [
-    "tuesday",
-    "http://www.billwurtz.com/tuesday.mp3"
-  ],
-  [
-    "youcandriveachevy.com",
-    "http://www.billwurtz.com/youcandriveachevydotcom.mp3"
-  ],
-  [
-    "i'm in bryant park",
-    "http://www.billwurtz.com/bryantpark.mp3"
-  ],
-  [
-    "don't look in a boot",
-    "http://www.billwurtz.com/dontlookinaboot.mp3"
-  ],
-  [
-    "we could just get high",
-    "http://www.billwurtz.com/wecouldjustgethigh.mp3"
-  ],
-  [
-    "icy james",
-    "http://www.billwurtz.com/icyjames.mp3"
-  ],
-  [
-    "the future song",
-    "http://www.billwurtz.com/thefuturesong.mp3"
-  ],
-  [
-    "rabbit snakes",
-    "http://www.billwurtz.com/rabbitsnakes.mp3"
-  ],
-  [
-    "school",
-    "http://www.billwurtz.com/school.mp3"
-  ],
-  [
-    "textin on my iphone",
-    "http://www.billwurtz.com/textinonmyiphone.mp3"
-  ],
-  [
-    "can i",
-    "http://www.billwurtz.com/can%20i.mp3"
-  ],
-  [
-    "how am i spost (Bill Wurtz ft. Flide Hamulton)",
-    "http://www.billwurtz.com/how%20am%20i%20spost%20(Bill%20Wurtz%20ft.%20Flide%20Hamulton).mp3"
-  ],
-  [
-    "hide the money",
-    "http://www.billwurtz.com/hide%20the%20money.mp3"
-  ],
-  [
-    "eat dirt",
-    "http://www.billwurtz.com/eat%20dirt.mp3"
-  ],
-  [
-    "teddy bears",
-    "http://www.billwurtz.com/teddy%20bears.mp3"
-  ],
-  [
-    "i write stupid music",
-    "http://www.billwurtz.com/i%20write%20stupid%20music.mp3"
-  ],
-  [
-    "go back to where you belong",
-    "http://www.billwurtz.com/go%20back%20to%20where%20you%20belong.mp3"
-  ],
-  [
-    "don't be good to your neighbor",
-    "http://www.billwurtz.com/don%27t%20be%20good%20to%20your%20neighbor.mp3"
-  ],
-  [
-    "hi, it's 1995",
-    "http://www.billwurtz.com/hi%20it%27s%201995.mp3"
-  ],
-  [
-    "hello 1",
-    "http://www.billwurtz.com/hello%201.mp3"
-  ],
-  [
-    "skip to my loo",
-    "http://www.billwurtz.com/skip%20to%20my%20loo.mp3"
-  ],
-  [
-    "hey baby, buy my car",
-    "http://www.billwurtz.com/hey%20baby,%20buy%20my%20car.mp3"
-  ],
-  [
-    "hello 2",
-    "http://www.billwurtz.com/hello%202.mp3"
-  ],
-  [
-    "move over",
-    "http://www.billwurtz.com/move%20over.mp3"
-  ],
-  [
-    "raindrops",
-    "http://www.billwurtz.com/raindrops.mp3"
-  ],
-  [
-    "lalala",
-    "http://www.billwurtz.com/lalala.mp3"
-  ],
-  [
-    "get outta here",
-    "http://www.billwurtz.com/get%20outta%20here.mp3"
-  ],
-  [
-    "get real",
-    "http://www.billwurtz.com/get%20real.mp3"
-  ],
-  [
-    "gross insides",
-    "http://www.billwurtz.com/gross%20insides.mp3"
-  ],
-  [
-    "hi bye",
-    "http://www.billwurtz.com/hi%20bye.mp3"
-  ],
-  [
-    "sheep",
-    "http://www.billwurtz.com/sheep.mp3"
-  ],
-  [
-    "instrumental: happy birfbday",
-    "http://www.billwurtz.com/Happy-Birfbday.mp3"
-  ],
-  [
-    "instrumental: juvenile (le nu jive)",
-    "http://www.billwurtz.com/juvenile%20(le%20nu%20jive).mp3"
-  ],
-  [
-    "instrumental: happy garden",
-    "http://www.billwurtz.com/happy%20garden.mp3"
-  ],
-  [
-    "instrumental: Unscheduled Psychiatric Confluence (UPC)",
-    "http://www.billwurtz.com/Unscheduled%20Psychiatric%20Confluence%20(UPC).mp3"
-  ],
-  [
-    "instrumental: Summer Love (Froy Thrampton ft. Bill Wurtz)",
-    "http://www.billwurtz.com/Summer%20Love%20(Froy%20Thrampton%20ft.%20Bill%20Wurtz).mp3"
-  ],
-  [
-    "instrumental: meadow music",
-    "http://www.billwurtz.com/meadow%20music.mp3"
-  ],
-  [
-    "write a tune that really sucks",
-    "http://www.billwurtz.com/write%20a%20tune%20that%20really%20sucks.mp3"
-  ],
-  [
-    "the zoo",
-    "http://www.billwurtz.com/the%20zoo.mp3"
-  ],
-  [
-    "jazz: in a mellow tone",
-    "http://www.billwurtz.com/inamellowtone.mp3"
-  ],
-  [
-    "Another Day",
-    "http://www.billwurtz.com/Another-Day.mp3"
-  ],
-  [
-    "jazz: look to the sky",
-    "http://www.billwurtz.com/looktotheskyjobim.mp3"
-  ],
-  [
-    "jazz: central park west wtf",
-    "http://www.billwurtz.com/centralparkwestwtf.mp3"
-  ],
-  [
-    "instrumental: mashup of 'clip 6 from 7 29 08'",
-    "http://www.billwurtz.com/mashup%20of%20clip%206%20from%207%2029%2008.mp3"
-  ],
-  [
-    "punk: freefallin",
-    "http://www.billwurtz.com/frre%20felln.mp3"
-  ],
-  [
-    "Stupid Song",
-    "http://www.billwurtz.com/Stupid%20Song.mp3"
-  ],
-  [
-    "I Guess I've Got to Listen to Bob Marley",
-    "http://www.billwurtz.com/I%20Guess%20I%27ve%20Got%20To%20Listen%20To%20Bob%20Marley.mp3"
-  ],
-  [
-    "Home",
-    "http://www.billwurtz.com/Home_1.mp3"
-  ],
-  [
-    "i'm so shy",
-    "http://www.billwurtz.com/I%27m%20So%20Shy.mp3"
-  ],
-  [
-    "the world",
-    "http://www.billwurtz.com/The%20World%27s%20Got%20a%20Problem.mp3"
-  ],
-  [
-    "All U Need Is Love",
-    "http://www.billwurtz.com/All%20U%20Need%20Is%20Love.mp3"
-  ],
-  [
-    "Do What You Want To Do",
-    "http://www.billwurtz.com/Do%20What%20You%20Want%20To%20Do.mp3"
-  ],
-  [
-    "(what) Love Is",
-    "http://www.billwurtz.com/(What)%20Love%20Is.mp3"
-  ],
-  [
-    "go to the store",
-    "http://www.billwurtz.com/Go%20to%20the%20Store.mp3"
-  ],
-  [
-    "i love you",
-    "http://www.billwurtz.com/I%20Love%20You.mp3"
-  ],
-  [
-    "do the thing",
-    "http://www.billwurtz.com/Do%20The%20Thing.mp3"
-  ],
-  [
-    "no place like home",
-    "http://www.billwurtz.com/No%20Place%20Like%20Home.mp3"
-  ],
-  [
-    "the stupid song",
-    "http://www.billwurtz.com/The%20Stupid%20Song.mp3"
-  ],
-  [
-    "Home Again",
-    "http://www.billwurtz.com/Home%20Again.mp3"
-  ],
-  [
-    "how am i spost",
-    "http://www.billwurtz.com/how%20am%20i%20spost.mp3"
-  ],
-  [
-    "blue boy",
-    "http://www.billwurtz.com/blue%20boy.mp3"
-  ],
-  [
-    "instrumental: ABCDE",
-    "http://www.billwurtz.com/ABCDE.mp3"
-  ],
-  [
-    "end of the world concert",
-    "http://www.billwurtz.com/end%20of%20the%20world%20concert%205%2021%2011%20450am.mp3"
-  ],
-  [
-    "instrumental: this funny bad flame -- ponso skidpul",
-    "http://www.billwurtz.com/this%20funny%20bad%20flame%20-%20ponso%20skidpul.mp3"
-  ],
-  [
-    "i like",
-    "http://www.billwurtz.com/i%20like.mp3"
-  ],
-  [
-    "instrumental: basic balloon seed iteration 1",
-    "http://www.billwurtz.com/basic%20balloon%20seed%20iteration%201.mp3"
-  ],
-  [
-    "dumpies - joe no",
-    "http://www.billwurtz.com/dumpies%20-%20joe%20no.mp3"
-  ],
-  [
-    "murder your demon",
-    "http://www.billwurtz.com/murder%20your%20demon.mp3"
-  ],
-  [
-    "dream of evil",
-    "http://www.billwurtz.com/dream%20of%20evil.mp3"
-  ],
-  [
-    "fever",
-    "http://www.billwurtz.com/fever%20(is%20what%20makes%20you%20sick).mp3"
-  ],
-  [
-    "i'm about to graduate from school",
-    "http://www.billwurtz.com/im%20about%20to%20graduate%20from%20school.mp3"
-  ],
-  [
-    "instrumental: relaxment stage 1",
-    "http://www.billwurtz.com/relaxment%20stage%201.mp3"
-  ],
-  [
-    "pairs",
-    "http://www.billwurtz.com/pairs.mp3"
-  ],
-  [
-    "grow mushrooms on the sidewalk",
-    "http://www.billwurtz.com/mushrooms%20on%20the%20sidewalk.mp3"
-  ],
-  [
-    "fuck you",
-    "http://www.billwurtz.com/fuck%20you.mp3"
-  ],
-  [
-    "whatever",
-    "http://www.billwurtz.com/whatever.mp3"
-  ],
-  [
-    "no",
-    "http://www.billwurtz.com/no.mp3"
-  ],
-  [
-    "yes",
-    "http://www.billwurtz.com/yes.mp3"
-  ],
-  [
-    "rock'n'roll confusion",
-    "http://www.billwurtz.com/rocknroll%20confusion.mp3"
-  ],
-  [
-    "the trees",
-    "http://www.billwurtz.com/the%20trees%20(okay,%20alright).mp3"
-  ],
-  [
-    "jazz over pilotwings",
-    "http://www.billwurtz.com/jazzoverpilotwings.mp3"
-  ],
-  [
-    "sickness as usual",
-    "http://www.billwurtz.com/sickness%20as%20usual.mp3"
-  ],
-  [
-    "i'm not sure",
-    "http://www.billwurtz.com/im%20not%20sure.mp3"
-  ],
-  [
-    "mcdonald's",
-    "http://www.billwurtz.com/mcdonalds.mp3"
-  ],
-  [
-    "fly away",
-    "http://www.billwurtz.com/fly%20away.mp3"
-  ],
-  [
-    "my penis",
-    "http://www.billwurtz.com/my%20penis.mp3"
-  ],
-  [
-    "sing me a song",
-    "http://www.billwurtz.com/sing%20me%20a%20song.mp3"
-  ],
-  [
-    "what the fuck",
-    "http://www.billwurtz.com/what%20the%20fuck.mp3"
-  ],
-  [
-    "hibernate",
-    "http://www.billwurtz.com/hibernate.mp3"
-  ],
-  [
-    "modern woes",
-    "http://www.billwurtz.com/modern%20woes.mp3"
-  ],
-  [
-    "eat bread (feel sure)",
-    "http://www.billwurtz.com/feel%20sure%20(eat%20bread).mp3"
-  ],
-  [
-    "2010",
-    "http://www.billwurtz.com/2010.mp3"
-  ],
-  [
-    "i'm sexy",
-    "http://www.billwurtz.com/i%27m%20sexy.mp3"
-  ],
-  [
-    "still silly",
-    "http://www.billwurtz.com/still%20silly.mp3"
-  ],
-  [
-    "lima beans",
-    "http://www.billwurtz.com/lima%20beans.mp3"
-  ],
-  [
-    "heebity deebities",
-    "http://www.billwurtz.com/heebity%20deebities.mp3"
-  ],
-  [
-    "barf on me",
-    "http://www.billwurtz.com/preface%20to%20creepy%20song.mp3"
-  ],
-  [
-    "song 41",
-    "http://www.billwurtz.com/song%2041.mp3"
-  ],
-  [
-    "tape deck",
-    "http://www.billwurtz.com/tape%20deck.mp3"
-  ],
-  [
-    "blind (to no avail)",
-    "http://www.billwurtz.com/blind%20(to%20no%20avail).mp3"
-  ],
-  [
-    "feel okay",
-    "http://www.billwurtz.com/feel%20okay.mp3"
-  ],
-  [
-    "no harm",
-    "http://www.billwurtz.com/no%20harm.mp3"
-  ],
-  [
-    "the summertime",
-    "http://www.billwurtz.com/the%20summertime.mp3"
-  ],
-  [
-    "we are humans",
-    "http://www.billwurtz.com/we%20are%20humans.mp3"
-  ],
-  [
-    "be someone",
-    "http://www.billwurtz.com/be%20someone.mp3"
-  ],
-  [
-    "make me write my songs",
-    "http://www.billwurtz.com/make%20me%20write%20my%20songs.mp3"
-  ],
-  [
-    "don't make",
-    "http://www.billwurtz.com/don%27t%20make.mp3"
-  ],
-  [
-    "we're all gonna be fine",
-    "http://www.billwurtz.com/were%20all%20gonna%20be%20fine.mp3"
-  ],
-  [
-    "desk and chair",
-    "http://www.billwurtz.com/desk%20and%20chair.mp3"
-  ],
-  [
-    "home from work",
-    "http://www.billwurtz.com/home%20from%20work.mp3"
-  ],
-  [
-    "dance the",
-    "http://www.billwurtz.com/dance%20the.mp3"
-  ],
-  [
-    "you can fly",
-    "http://www.billwurtz.com/you%20can%20fly.mp3"
-  ],
-  [
-    "i wanna be with you",
-    "http://www.billwurtz.com/i%20wanna%20be%20with%20you.mp3"
-  ],
-  [
-    "be free and don't sell records",
-    "http://www.billwurtz.com/be%20free%20and%20don%27t%20sell%20records.mp3"
-  ],
-  [
-    "wondering how to feel",
-    "http://www.billwurtz.com/wondering%20how%20to%20feel.mp3"
-  ],
-  [
-    "the ladies",
-    "http://www.billwurtz.com/the%20ladies.mp3"
-  ],
-  [
-    "i'm sad",
-    "http://www.billwurtz.com/i%27m%20sad.mp3"
-  ],
-  [
-    "i wanna go home",
-    "http://www.billwurtz.com/i%20wanna%20go%20home.mp3"
-  ],
-  [
-    "not so hard to do",
-    "http://www.billwurtz.com/not%20so%20hard%20to%20do.mp3"
-  ],
-  [
-    "instrumental: i'm alive",
-    "http://www.billwurtz.com/im%20alive.mp3"
-  ],
-  [
-    "instrumental: groove thing",
-    "http://www.billwurtz.com/groove%20thing.mp3"
-  ],
-  [
-    "shut up",
-    "http://www.billwurtz.com/shut%20up.mp3"
-  ],
-  [
-    "don't talk to me",
-    "http://www.billwurtz.com/don%27t%20talk%20to%20me.mp3"
-  ],
-  [
-    "magnet",
-    "http://www.billwurtz.com/magnet.mp3"
-  ],
-  [
-    "diamond",
-    "http://www.billwurtz.com/diamond.mp3"
-  ],
-  [
-    "instrumental: no castle",
-    "http://www.billwurtz.com/no%20castle.mp3"
-  ],
-  [
-    "feathered box",
-    "http://www.billwurtz.com/feathered%20box.mp3"
-  ],
-  [
-    "instrumental: wind and raeihgn",
-    "http://www.billwurtz.com/wind%20and%20raeihgn.mp3"
-  ],
-  [
-    "instrumental: this song yeah",
-    "http://www.billwurtz.com/this%20song%20yeah.mp3"
-  ],
-  [
-    "bears",
-    "http://www.billwurtz.com/bears.mp3"
-  ],
-  [
-    "die",
-    "http://www.billwurtz.com/die.mp3"
-  ],
-  [
-    "little door",
-    "http://www.billwurtz.com/little%20door.mp3"
-  ],
-  [
-    "couldn't succeed",
-    "http://www.billwurtz.com/couldnt%20succeed.mp3"
-  ],
-  [
-    "go home",
-    "http://www.billwurtz.com/go%20home.mp3"
-  ],
-  [
-    "napkins",
-    "http://www.billwurtz.com/napkins.mp3"
-  ],
-  [
-    "burger king",
-    "http://www.billwurtz.com/burger%20king.mp3"
-  ],
-  [
-    "instrumental: grand piano demo",
-    "http://www.billwurtz.com/grand%20piano%20demo.mp3"
-  ],
-  [
-    "instrumental: my sad song",
-    "http://www.billwurtz.com/my%20sad%20song.mp3"
-  ],
-  [
-    "instrumental: thing 1 10 10 ",
-    "http://www.billwurtz.com/11010%20the%20thing.mp3"
-  ],
-  [
-    "15 minutes",
-    "http://www.billwurtz.com/precursor.mp3"
-  ],
-  [
-    "instrumental: window pain",
-    "http://www.billwurtz.com/window%20pain.mp3"
-  ],
-  [
-    "punk: who am i (iris)",
-    "http://www.billwurtz.com/iris.mp3"
-  ],
-  [
-    "instrumental: jungle mania",
-    "http://www.billwurtz.com/jungle%20mania.mp3"
-  ],
-  [
-    "instrumental: good night",
-    "http://www.billwurtz.com/good%20night.mp3"
-  ],
-  [
-    "instrumental: aimless",
-    "http://www.billwurtz.com/aimless.mp3"
-  ],
-  [
-    "instrumental: music time",
-    "http://www.billwurtz.com/music%20time.mp3"
-  ],
-  [
-    "instrumental: planet earth",
-    "http://www.billwurtz.com/planet%20earth.mp3"
-  ],
-  [
-    "Marketing Telephono ft. Chris Aiello",
-    "http://www.billwurtz.com/marketing%20telephono.mp3"
-  ],
-  [
-    "the song song",
-    "http://www.billwurtz.com/THE%20SONG%20SONG.mp3"
-  ],
-  [
-    "instrumental: typhoon sample measuring kit",
-    "http://www.billwurtz.com/typhoon%20sample%20measuring%20kit.mp3"
-  ],
-  [
-    "instrumental: time mop",
-    "http://www.billwurtz.com/time%20mop.mp3"
-  ],
-  [
-    "instrumental: wrecked music of the state capital",
-    "http://www.billwurtz.com/wrecked%20music%20of%20the%20state%20capital.mp3"
-  ],
-  [
-    "instrumental: help me, i'm trapped in the center of a rectangle",
-    "http://www.billwurtz.com/help%20me,%20i%27m%20trapped%20in%20the%20center%20of%20a%20rectangle.mp3"
-  ],
-  [
-    "punk: your body is a wonderland",
-    "http://www.billwurtz.com/yourbodyisawonderland.mp3"
-  ],
-  [
-    "punk: welcome to the jungle",
-    "http://www.billwurtz.com/rule.mp3"
-  ],
-  [
-    "instrumental: paradise",
-    "http://www.billwurtz.com/paradise.mp3"
-  ],
-  [
-    "instrumental: Trade X",
-    "http://www.billwurtz.com/Trade%20X.mp3"
-  ],
-  [
-    "stuck in a rut",
-    "http://www.billwurtz.com/rut.mp3"
-  ],
-  [
-    "instrumental: illuminated glass container",
-    "http://www.billwurtz.com/igc.mp3"
-  ],
-  [
-    "instrumental: frontier cave",
-    "http://www.billwurtz.com/frontiercave.mp3"
-  ],
-  [
-    "instrumental: sincere sam",
-    "http://www.billwurtz.com/sinceresam.mp3"
-  ],
-  [
-    "instrumental: Late Nite Lounge with Loud Lenny",
-    "http://www.billwurtz.com/late-nite-lounge-with-loud-lenny.mp3"
-  ]
-]
diff --git a/bill-instrumentals.json b/bill-instrumentals.json
deleted file mode 100644
index b63f790..0000000
--- a/bill-instrumentals.json
+++ /dev/null
@@ -1,182 +0,0 @@
-[
-  [
-    "hey mom i beat the scale!",
-    "http://www.billwurtz.com/hey-mom-i-beat-the-scale.mp3"
-  ],
-  [
-    "happy birfbday",
-    "http://www.billwurtz.com/Happy-Birfbday.mp3"
-  ],
-  [
-    "juvenile (le nu jive)",
-    "http://www.billwurtz.com/juvenile%20(le%20nu%20jive).mp3"
-  ],
-  [
-    "happy garden",
-    "http://www.billwurtz.com/happy%20garden.mp3"
-  ],
-  [
-    "Unscheduled Psychiatric Confluence (UPC)",
-    "http://www.billwurtz.com/Unscheduled%20Psychiatric%20Confluence%20(UPC).mp3"
-  ],
-  [
-    "Summer Love (Froy Thrampton ft. Bill Wurtz)",
-    "http://www.billwurtz.com/Summer%20Love%20(Froy%20Thrampton%20ft.%20Bill%20Wurtz).mp3"
-  ],
-  [
-    "meadow music",
-    "http://www.billwurtz.com/meadow%20music.mp3"
-  ],
-  [
-    "jazz: in a mellow tone",
-    "http://www.billwurtz.com/inamellowtone.mp3"
-  ],
-  [
-    "jazz: look to the sky",
-    "http://www.billwurtz.com/looktotheskyjobim.mp3"
-  ],
-  [
-    "jazz: central park west wtf",
-    "http://www.billwurtz.com/centralparkwestwtf.mp3"
-  ],
-  [
-    "mashup of 'clip 6 from 7 29 08'",
-    "http://www.billwurtz.com/mashup%20of%20clip%206%20from%207%2029%2008.mp3"
-  ],
-  [
-    "ABCDE",
-    "http://www.billwurtz.com/ABCDE.mp3"
-  ],
-  [
-    "end of the world concert",
-    "http://www.billwurtz.com/end%20of%20the%20world%20concert%205%2021%2011%20450am.mp3"
-  ],
-  [
-    "this funny bad flame -- ponso skidpul",
-    "http://www.billwurtz.com/this%20funny%20bad%20flame%20-%20ponso%20skidpul.mp3"
-  ],
-  [
-    "basic balloon seed iteration 1",
-    "http://www.billwurtz.com/basic%20balloon%20seed%20iteration%201.mp3"
-  ],
-  [
-    "relaxment stage 1",
-    "http://www.billwurtz.com/relaxment%20stage%201.mp3"
-  ],
-  [
-    "jazz over pilotwings",
-    "http://www.billwurtz.com/jazzoverpilotwings.mp3"
-  ],
-  [
-    "i'm alive",
-    "http://www.billwurtz.com/im%20alive.mp3"
-  ],
-  [
-    "groove thing",
-    "http://www.billwurtz.com/groove%20thing.mp3"
-  ],
-  [
-    "no castle",
-    "http://www.billwurtz.com/no%20castle.mp3"
-  ],
-  [
-    "wind and raeihgn",
-    "http://www.billwurtz.com/wind%20and%20raeihgn.mp3"
-  ],
-  [
-    "this song yeah",
-    "http://www.billwurtz.com/this%20song%20yeah.mp3"
-  ],
-  [
-    "grand piano demo",
-    "http://www.billwurtz.com/grand%20piano%20demo.mp3"
-  ],
-  [
-    "my sad song",
-    "http://www.billwurtz.com/my%20sad%20song.mp3"
-  ],
-  [
-    "thing 1 10 10 ",
-    "http://www.billwurtz.com/11010%20the%20thing.mp3"
-  ],
-  [
-    "window pain",
-    "http://www.billwurtz.com/window%20pain.mp3"
-  ],
-  [
-    "punk: who am i (iris)",
-    "http://www.billwurtz.com/iris.mp3"
-  ],
-  [
-    "jungle mania",
-    "http://www.billwurtz.com/jungle%20mania.mp3"
-  ],
-  [
-    "good night",
-    "http://www.billwurtz.com/good%20night.mp3"
-  ],
-  [
-    "aimless",
-    "http://www.billwurtz.com/aimless.mp3"
-  ],
-  [
-    "music time",
-    "http://www.billwurtz.com/music%20time.mp3"
-  ],
-  [
-    "planet earth",
-    "http://www.billwurtz.com/planet%20earth.mp3"
-  ],
-  [
-    "typhoon sample measuring kit",
-    "http://www.billwurtz.com/typhoon%20sample%20measuring%20kit.mp3"
-  ],
-  [
-    "time mop",
-    "http://www.billwurtz.com/time%20mop.mp3"
-  ],
-  [
-    "wrecked music of the state capital",
-    "http://www.billwurtz.com/wrecked%20music%20of%20the%20state%20capital.mp3"
-  ],
-  [
-    "help me, i'm trapped in the center of a rectangle",
-    "http://www.billwurtz.com/help%20me,%20i%27m%20trapped%20in%20the%20center%20of%20a%20rectangle.mp3"
-  ],
-  [
-    "punk: your body is a wonderland",
-    "http://www.billwurtz.com/yourbodyisawonderland.mp3"
-  ],
-  [
-    "punk: welcome to the jungle",
-    "http://www.billwurtz.com/rule.mp3"
-  ],
-  [
-    "paradise",
-    "http://www.billwurtz.com/paradise.mp3"
-  ],
-  [
-    "Trade X",
-    "http://www.billwurtz.com/Trade%20X.mp3"
-  ],
-  [
-    "reggae car dealership",
-    "http://www.billwurtz.com/reggae-car-dealership.mp3"
-  ],
-  [
-    "illuminated glass container",
-    "http://www.billwurtz.com/igc.mp3"
-  ],
-  [
-    "frontier cave",
-    "http://www.billwurtz.com/frontiercave.mp3"
-  ],
-  [
-    "sincere sam",
-    "http://www.billwurtz.com/sinceresam.mp3"
-  ],
-  [
-    "Late Nite Lounge with Loud Lenny",
-    "http://www.billwurtz.com/late-nite-lounge-with-loud-lenny.mp3"
-  ]
-]
diff --git a/play.js b/play.js
deleted file mode 100644
index bdee7f4..0000000
--- a/play.js
+++ /dev/null
@@ -1,457 +0,0 @@
-// TODO: Get `avconv` working. Oftentimes `play` won't be able to play
-//       some tracks due to an unsupported format; we'll need to use
-//       `avconv` to convert them (to WAV).
-//       (Done!)
-//
-// TODO: Get `play` working.
-//       (Done!)
-//
-// TODO: Get play-next working; probably just act like a shuffle. Will
-//       need to keep an eye out for the `play` process finishing.
-//       (Done!)
-//
-// TODO: Preemptively download and process the next track, while the
-//       current one is playing, to eliminate the silent time between
-//       tracks.
-//       (Done!)
-//
-// TODO: Delete old tracks! Since we aren't overwriting files, we
-//       need to manually delete files once we're done with them.
-//       (Done!)
-//
-// TODO: Clean up on SIGINT.
-//
-// TODO: Get library filter path from stdin.
-//       (Done!)
-//
-// TODO: Show library tree. Do this AFTER filtering, so that people
-//       can e.g. see all albums by a specific artist.
-//       (Done!)
-//
-// TODO: Ignore .DS_Store.
-//       (Done!)
-//
-// TODO: Have a download timeout, somehow.
-//
-// TODO: Fix the actual group format. Often times we get single-letter
-//       files being downloaded (which don't exist); I'm guessing that's
-//       related to folder names (which are just strings, not title-href
-//       arrays) still being in the group array. (Update: that's defin-
-//       itely true; 'Saucey Sounds'[0] === 'S', and 'Unofficial'[0]
-//       === 'U', which are the two "files" it crashes on while playing
-//       -g 'Jake Chudnow'.)
-//       (Done!)
-//
-// TODO: A way to exclude a specific group path.
-//       (Done!)
-//
-// TODO: Better argv handling.
-//       (Done!)
-//
-// TODO: Option to include a specific path from the source playlist.
-//       (Done!)
-//
-// TODO: Make a playlist generator that parses http://billwurtz.com
-//       instrumentals.html.
-//       (Done!)
-//
-// TODO: Make crawl-itunes.js a bit more general, more command-line
-//       friendly (i.e. don't require editing the script itself), and
-//       make it use the getHTMLLinks function defined in the new
-//       crawl-links.js script.
-//       (Done!)
-//
-// TODO: Play-in-order track picker.
-//       (Done!)
-
-'use strict'
-
-const fs = require('fs')
-const util = require('util')
-const { spawn } = require('child_process')
-
-const fetch = require('node-fetch')
-const sanitize = require('sanitize-filename')
-
-const writeFile = util.promisify(fs.writeFile)
-const readFile = util.promisify(fs.readFile)
-const unlink = util.promisify(fs.unlink)
-
-function promisifyProcess(proc, showLogging = true) {
-	return new Promise((resolve, reject) => {
-		if (showLogging) {
-			proc.stdout.pipe(process.stdout)
-			proc.stderr.pipe(process.stderr)
-		}
-
-		proc.on('exit', code => {
-			if (code === 0) {
-				resolve()
-			} else {
-				console.error('Process failed!', proc.spawnargs)
-				reject(code)
-			}
-		})
-	})
-}
-
-function flattenPlaylist(playlist) {
-	const groups = playlist.filter(x => Array.isArray(x[1]))
-	const nonGroups = playlist.filter(x => x[1] && !(Array.isArray(x[1])))
-	return groups.map(g => flattenPlaylist(g[1]))
-		.reduce((a, b) => a.concat(b), nonGroups)
-}
-
-function convert(fromFile, toFile) {
-	const avconv = spawn('avconv', ['-y', '-i', fromFile, toFile])
-	return promisifyProcess(avconv, false)
-}
-
-function playFile(file) {
-	const play = spawn('play', [file])
-	return promisifyProcess(play)
-}
-
-function makeOrderedPlaylistPicker(playlist) {
-	const allSongs = flattenPlaylist(playlist)
-	let index = 0
-
-	return function() {
-		if (index < allSongs.length) {
-			const picked = allSongs[index]
-			index++
-			return picked
-		} else {
-			return null
-		}
-	}
-}
-
-function makeShufflePlaylistPicker(playlist) {
-	const allSongs = flattenPlaylist(playlist)
-
-	return function() {
-		const index = Math.floor(Math.random() * allSongs.length)
-		const picked = allSongs[index]
-		return picked
-	}
-}
-
-async function loopPlay(fn) {
-	// Looping play function. Takes one argument, the "pick" function,
-	// which returns a track to play. Preemptively downloads the next
-	// track while the current one is playing for seamless continuation
-	// from one song to the next. Stops when the result of the pick
-	// function is null (or similar).
-
-	async function downloadNext() {
-		const picked = fn()
-
-		if (picked == null) {
-			return false
-		}
-
-		const [ title, href ] = picked
-		console.log(`Downloading ${title}..\n${href}`)
-
-		const wavFile = `.${sanitize(title)}.wav`
-
-		const res = await fetch(href)
-		const buffer = await res.buffer()
-		await writeFile('./.temp-track', buffer)
-
-		try {
-			await convert('./.temp-track', wavFile)
-		} catch(err) {
-			console.warn('Failed to convert ' + title)
-			console.warn('Selecting a new track\n')
-
-			return await downloadNext()
-		}
-
-		await unlink('./.temp-track')
-
-		return wavFile
-	}
-
-	let wavFile = await downloadNext()
-
-	while (wavFile) {
-		const nextPromise = downloadNext()
-		await playFile(wavFile)
-		await unlink(wavFile)
-		wavFile = await nextPromise
-	}
-}
-
-function filterPlaylistByPathString(playlist, pathString) {
-	return filterPlaylistByPath(playlist, parsePathString(pathString))
-}
-
-function filterPlaylistByPath(playlist, pathParts) {
-	// Note this can be used as a utility function, rather than just as
-	// a function for use by the argv-handler!
-
-	let cur = pathParts[0]
-
-	const match = playlist.find(g => g[0] === cur || g[0] === cur + '/')
-
-	if (match) {
-		const groupContents = match[1]
-		if (pathParts.length > 1) {
-			const rest = pathParts.slice(1)
-			return filterPlaylistByPath(groupContents, rest)
-		} else {
-			return match
-		}
-	} else {
-		console.warn(`Not found: "${cur}"`)
-		return playlist
-	}
-}
-
-function ignoreGroupByPathString(playlist, pathString) {
-	const pathParts = parsePathString(pathString)
-	return ignoreGroupByPath(playlist, pathParts)
-}
-
-function ignoreGroupByPath(playlist, pathParts) {
-	// TODO: Ideally this wouldn't mutate the given playlist.
-
-	const groupToRemove = filterPlaylistByPath(playlist, pathParts)
-
-	const parentPath = pathParts.slice(0, pathParts.length - 1)
-	let parent
-
-	if (parentPath.length === 0) {
-		parent = playlist
-	} else {
-		parent = filterPlaylistByPath(playlist, pathParts.slice(0, -1))
-	}
-
-	const index = parent.indexOf(groupToRemove)
-
-	if (index >= 0) {
-		parent.splice(index, 1)
-	} else {
-		console.error(
-			'Group ' + pathParts.join('/') + ' doesn\'t exist, so we can\'t ' +
-			'explicitly ignore it.'
-		)
-	}
-}
-
-function getPlaylistTreeString(playlist, showTracks = false) {
-	function recursive(group) {
-		const groups = group.filter(x => Array.isArray(x[1]))
-		const nonGroups = group.filter(x => x[1] && !(Array.isArray(x[1])))
-
-		const childrenString = groups.map(g => {
-			const groupString = recursive(g[1])
-
-			if (groupString) {
-				const indented = groupString.split('\n').map(l => '| ' + l).join('\n')
-				return '\n' + g[0] + '\n' + indented
-			} else {
-				return g[0]
-			}
-		}).join('\n')
-
-		const tracksString = (showTracks ? nonGroups.map(g => g[0]).join('\n') : '')
-
-		if (tracksString && childrenString) {
-			return tracksString + '\n' + childrenString
-		} else if (childrenString) {
-			return childrenString
-		} else if (tracksString) {
-			return tracksString
-		} else {
-			return ''
-		}
-	}
-
-	return recursive(playlist)
-}
-
-function parsePathString(pathString) {
-	const pathParts = pathString.split('/')
-	return pathParts
-}
-
-async function processArgv(argv, handlers) {
-	let i = 0
-
-	async function handleOpt(opt) {
-		if (opt in handlers) {
-			await handlers[opt]({
-				argv, index: i,
-				nextArg: function() {
-					i++
-					return argv[i]
-				},
-				alias: function(optionToRun) {
-					handleOpt(optionToRun)
-				}
-			})
-		} else {
-			console.warn('Option not understood: ' + opt)
-		}
-	}
-
-	for (; i < argv.length; i++) {
-		const cur = argv[i]
-		if (cur.startsWith('-')) {
-			const opt = cur.slice(1)
-			await handleOpt(opt)
-		}
-	}
-}
-
-readFile('./playlist.json', 'utf-8')
-	.then(plText => JSON.parse(plText))
-	.then(async playlist => {
-		let sourcePlaylist = playlist
-		let curPlaylist = playlist
-
-		let pickerType = 'shuffle'
-
-		// WILL play says whether the user has forced playback via an argument.
-		// SHOULD play says whether the program has automatically decided to play
-		// or not, if the user hasn't set WILL play.
-		let shouldPlay = true
-		let willPlay = null
-
-		await processArgv(process.argv, {
-			'-open': async function(util) {
-				// --open <file>  (alias: -o)
-				// Opens a separate playlist file.
-				// This sets the source playlist.
-
-				const openedPlaylist = JSON.parse(await readFile(util.nextArg(), 'utf-8'))
-				sourcePlaylist = openedPlaylist
-				curPlaylist = openedPlaylist
-			},
-
-			'o': util => util.alias('-open'),
-
-			'-clear': function(util) {
-				// --clear  (alias: -c)
-				// Clears the active playlist. This does not affect the source
-				// playlist.
-
-				curPlaylist = []
-			},
-
-			'c': util => util.alias('-clear'),
-
-			'-keep': function(util) {
-				// --keep <groupPath>  (alias: -k)
-				// Keeps a group by loading it from the source playlist into the
-				// active playlist. This is usually useful after clearing the
-				// active playlist; it can also be used to keep a subgroup when
-				// you've ignored an entire parent group, e.g. `-i foo -k foo/baz`.
-
-				const pathString = util.nextArg()
-				const group = filterPlaylistByPathString(sourcePlaylist, pathString)
-				curPlaylist.push(group)
-			},
-
-			'k': util => util.alias('-keep'),
-
-			'-ignore': function(util) {
-				// --ignore <groupPath>  (alias: -i)
-				// Filters the playlist so that the given path is removed.
-
-				const pathString = util.nextArg()
-				console.log('Ignoring path: ' + pathString)
-				ignoreGroupByPathString(curPlaylist, pathString)
-			},
-
-			'i': util => util.alias('-ignore'),
-
-			'-list-groups': function(util) {
-				// --list-groups  (alias: -l, --list)
-				// Lists all groups in the playlist.
-
-				console.log(getPlaylistTreeString(curPlaylist))
-
-				// If this is the last item in the argument list, the user probably
-				// only wants to get the list, so we'll mark the 'should run' flag
-				// as false.
-				if (util.index === util.argv.length - 1) {
-					shouldPlay = false
-				}
-			},
-
-			'-list': util => util.alias('-list-groups'),
-			'l': util => util.alias('-list-groups'),
-
-			'-list-all': function(util) {
-				// --list-all  (alias: --list-tracks, -L)
-				// Lists all groups and tracks in the playlist.
-
-				console.log(getPlaylistTreeString(curPlaylist, true))
-
-				// As with -l, if this is the last item in the argument list, we
-				// won't actually be playing the playlist.
-				if (util.index === util.argv.length - 1) {
-					shouldPlay = false
-				}
-			},
-
-			'-list-tracks': util => util.alias('-list-all'),
-			'L': util => util.alias('-list-all'),
-
-			'-play': function(util) {
-				// --play  (alias: -p)
-				// Forces the playlist to actually play.
-
-				willPlay = true
-			},
-
-			'p': util => util.alias('-play'),
-
-			'-no-play': function(util) {
-				// --no-play  (alias: -np)
-				// Forces the playlist not to play.
-
-				willPlay = false
-			},
-
-			'np': util => util.alias('-no-play'),
-
-			'-debug-list': function(util) {
-				// --debug-list
-				// Prints out the JSON representation of the active playlist.
-
-				console.log(JSON.stringify(curPlaylist, null, 2))
-			},
-
-			'-picker': function(util) {
-				// --picker <shuffle|ordered>
-				// Selects the mode that the song to play is picked.
-				// This should be used after finishing modifying the active
-				// playlist.
-
-				pickerType = util.nextArg()
-			}
-		})
-
-		if (willPlay || (willPlay === null && shouldPlay)) {
-			let picker
-			if (pickerType === 'shuffle') {
-				console.log('Using shuffle picker')
-				picker = makeShufflePlaylistPicker(curPlaylist)
-			} else if (pickerType === 'ordered') {
-				console.log('Using ordered picker')
-				picker = makeOrderedPlaylistPicker(curPlaylist)
-			} else {
-				console.error('Invalid picker type: ' + pickerType)
-			}
-
-			return loopPlay(picker)
-		} else {
-			return curPlaylist
-		}
-	})
-	.catch(err => console.error(err))
diff --git a/src/loop-play.js b/src/loop-play.js
new file mode 100644
index 0000000..e59fbc2
--- /dev/null
+++ b/src/loop-play.js
@@ -0,0 +1,69 @@
+'use strict'
+
+const fs = require('fs')
+
+const { spawn } = require('child_process')
+const { promisify } = require('util')
+const fetch = require('node-fetch')
+const sanitize = require('sanitize-filename')
+const promisifyProcess = require('./promisify-process')
+
+const writeFile = promisify(fs.writeFile)
+const unlink = promisify(fs.unlink)
+
+module.exports = async function loopPlay(fn) {
+  // Looping play function. Takes one argument, the "pick" function,
+  // which returns a track to play. Preemptively downloads the next
+  // track while the current one is playing for seamless continuation
+  // from one song to the next. Stops when the result of the pick
+  // function is null (or similar).
+
+  async function downloadNext() {
+    const picked = fn()
+
+    if (picked == null) {
+      return false
+    }
+
+    const [ title, href ] = picked
+    console.log(`Downloading ${title}..\n${href}`)
+
+    const wavFile = `.${sanitize(title)}.wav`
+
+    const res = await fetch(href)
+    const buffer = await res.buffer()
+    await writeFile('./.temp-track', buffer)
+
+    try {
+      await convert('./.temp-track', wavFile)
+    } catch(err) {
+      console.warn('Failed to convert ' + title)
+      console.warn('Selecting a new track\n')
+
+      return await downloadNext()
+    }
+
+    await unlink('./.temp-track')
+
+    return wavFile
+  }
+
+  let wavFile = await downloadNext()
+
+  while (wavFile) {
+    const nextPromise = downloadNext()
+    await playFile(wavFile)
+    await unlink(wavFile)
+    wavFile = await nextPromise
+  }
+}
+
+function convert(fromFile, toFile) {
+  const avconv = spawn('avconv', ['-y', '-i', fromFile, toFile])
+  return promisifyProcess(avconv, false)
+}
+
+function playFile(file) {
+  const play = spawn('play', [file])
+  return promisifyProcess(play)
+}
diff --git a/src/pickers.js b/src/pickers.js
new file mode 100644
index 0000000..236f9ea
--- /dev/null
+++ b/src/pickers.js
@@ -0,0 +1,33 @@
+'use strict'
+
+const { flattenPlaylist } = require('./playlist-utils')
+
+function makeOrderedPlaylistPicker(playlist) {
+  const allSongs = flattenPlaylist(playlist)
+  let index = 0
+
+  return function() {
+    if (index < allSongs.length) {
+      const picked = allSongs[index]
+      index++
+      return picked
+    } else {
+      return null
+    }
+  }
+}
+
+function makeShufflePlaylistPicker(playlist) {
+  const allSongs = flattenPlaylist(playlist)
+
+  return function() {
+    const index = Math.floor(Math.random() * allSongs.length)
+    const picked = allSongs[index]
+    return picked
+  }
+}
+
+module.exports = {
+  makeOrderedPlaylistPicker,
+  makeShufflePlaylistPicker
+}
diff --git a/src/play.js b/src/play.js
new file mode 100644
index 0000000..b0014f5
--- /dev/null
+++ b/src/play.js
@@ -0,0 +1,159 @@
+'use strict'
+
+const fs = require('fs')
+
+const { promisify } = require('util')
+const loopPlay = require('./loop-play')
+const processArgv = require('./process-argv')
+const pickers = require('./pickers')
+
+const readFile = promisify(fs.readFile)
+
+readFile('./playlist.json', 'utf-8')
+  .then(plText => JSON.parse(plText))
+  .then(async playlist => {
+    let sourcePlaylist = playlist
+    let curPlaylist = playlist
+
+    let pickerType = 'shuffle'
+
+    // WILL play says whether the user has forced playback via an argument.
+    // SHOULD play says whether the program has automatically decided to play
+    // or not, if the user hasn't set WILL play.
+    let shouldPlay = true
+    let willPlay = null
+
+    await processArgv(process.argv, {
+      '-open': async function(util) {
+        // --open <file>  (alias: -o)
+        // Opens a separate playlist file.
+        // This sets the source playlist.
+
+        const openedPlaylist = JSON.parse(await readFile(util.nextArg(), 'utf-8'))
+        sourcePlaylist = openedPlaylist
+        curPlaylist = openedPlaylist
+      },
+
+      'o': util => util.alias('-open'),
+
+      '-clear': function(util) {
+        // --clear  (alias: -c)
+        // Clears the active playlist. This does not affect the source
+        // playlist.
+
+        curPlaylist = []
+      },
+
+      'c': util => util.alias('-clear'),
+
+      '-keep': function(util) {
+        // --keep <groupPath>  (alias: -k)
+        // Keeps a group by loading it from the source playlist into the
+        // active playlist. This is usually useful after clearing the
+        // active playlist; it can also be used to keep a subgroup when
+        // you've ignored an entire parent group, e.g. `-i foo -k foo/baz`.
+
+        const pathString = util.nextArg()
+        const group = filterPlaylistByPathString(sourcePlaylist, pathString)
+        curPlaylist.push(group)
+      },
+
+      'k': util => util.alias('-keep'),
+
+      '-ignore': function(util) {
+        // --ignore <groupPath>  (alias: -i)
+        // Filters the playlist so that the given path is removed.
+
+        const pathString = util.nextArg()
+        console.log('Ignoring path: ' + pathString)
+        ignoreGroupByPathString(curPlaylist, pathString)
+      },
+
+      'i': util => util.alias('-ignore'),
+
+      '-list-groups': function(util) {
+        // --list-groups  (alias: -l, --list)
+        // Lists all groups in the playlist.
+
+        console.log(getPlaylistTreeString(curPlaylist))
+
+        // If this is the last item in the argument list, the user probably
+        // only wants to get the list, so we'll mark the 'should run' flag
+        // as false.
+        if (util.index === util.argv.length - 1) {
+          shouldPlay = false
+        }
+      },
+
+      '-list': util => util.alias('-list-groups'),
+      'l': util => util.alias('-list-groups'),
+
+      '-list-all': function(util) {
+        // --list-all  (alias: --list-tracks, -L)
+        // Lists all groups and tracks in the playlist.
+
+        console.log(getPlaylistTreeString(curPlaylist, true))
+
+        // As with -l, if this is the last item in the argument list, we
+        // won't actually be playing the playlist.
+        if (util.index === util.argv.length - 1) {
+          shouldPlay = false
+        }
+      },
+
+      '-list-tracks': util => util.alias('-list-all'),
+      'L': util => util.alias('-list-all'),
+
+      '-play': function(util) {
+        // --play  (alias: -p)
+        // Forces the playlist to actually play.
+
+        willPlay = true
+      },
+
+      'p': util => util.alias('-play'),
+
+      '-no-play': function(util) {
+        // --no-play  (alias: -np)
+        // Forces the playlist not to play.
+
+        willPlay = false
+      },
+
+      'np': util => util.alias('-no-play'),
+
+      '-debug-list': function(util) {
+        // --debug-list
+        // Prints out the JSON representation of the active playlist.
+
+        console.log(JSON.stringify(curPlaylist, null, 2))
+      },
+
+      '-picker': function(util) {
+        // --picker <shuffle|ordered>
+        // Selects the mode that the song to play is picked.
+        // This should be used after finishing modifying the active
+        // playlist.
+
+        pickerType = util.nextArg()
+      }
+    })
+
+    if (willPlay || (willPlay === null && shouldPlay)) {
+      let picker
+      if (pickerType === 'shuffle') {
+        console.log('Using shuffle picker')
+        picker = pickers.makeShufflePlaylistPicker(curPlaylist)
+      } else if (pickerType === 'ordered') {
+        console.log('Using ordered picker')
+        picker = pickers.makeOrderedPlaylistPicker(curPlaylist)
+      } else {
+        console.error('Invalid picker type: ' + pickerType)
+      }
+
+      return loopPlay(picker)
+    } else {
+      return curPlaylist
+    }
+  })
+  .catch(err => console.error(err))
diff --git a/src/playlist-utils.js b/src/playlist-utils.js
new file mode 100644
index 0000000..d853456
--- /dev/null
+++ b/src/playlist-utils.js
@@ -0,0 +1,109 @@
+'use strict'
+
+function flattenPlaylist(playlist) {
+  const groups = playlist.filter(x => Array.isArray(x[1]))
+  const nonGroups = playlist.filter(x => x[1] && !(Array.isArray(x[1])))
+  return groups.map(g => flattenPlaylist(g[1]))
+    .reduce((a, b) => a.concat(b), nonGroups)
+}
+
+function filterPlaylistByPathString(playlist, pathString) {
+  return filterPlaylistByPath(playlist, parsePathString(pathString))
+}
+
+function filterPlaylistByPath(playlist, pathParts) {
+  // Note this can be used as a utility function, rather than just as
+  // a function for use by the argv-handler!
+
+  let cur = pathParts[0]
+
+  const match = playlist.find(g => g[0] === cur || g[0] === cur + '/')
+
+  if (match) {
+    const groupContents = match[1]
+    if (pathParts.length > 1) {
+      const rest = pathParts.slice(1)
+      return filterPlaylistByPath(groupContents, rest)
+    } else {
+      return match
+    }
+  } else {
+    console.warn(`Not found: "${cur}"`)
+    return playlist
+  }
+}
+
+function ignoreGroupByPathString(playlist, pathString) {
+  const pathParts = parsePathString(pathString)
+  return ignoreGroupByPath(playlist, pathParts)
+}
+
+function ignoreGroupByPath(playlist, pathParts) {
+  // TODO: Ideally this wouldn't mutate the given playlist.
+
+  const groupToRemove = filterPlaylistByPath(playlist, pathParts)
+
+  const parentPath = pathParts.slice(0, pathParts.length - 1)
+  let parent
+
+  if (parentPath.length === 0) {
+    parent = playlist
+  } else {
+    parent = filterPlaylistByPath(playlist, pathParts.slice(0, -1))
+  }
+
+  const index = parent.indexOf(groupToRemove)
+
+  if (index >= 0) {
+    parent.splice(index, 1)
+  } else {
+    console.error(
+      'Group ' + pathParts.join('/') + ' doesn\'t exist, so we can\'t ' +
+      'explicitly ignore it.'
+    )
+  }
+}
+
+function getPlaylistTreeString(playlist, showTracks = false) {
+  function recursive(group) {
+    const groups = group.filter(x => Array.isArray(x[1]))
+    const nonGroups = group.filter(x => x[1] && !(Array.isArray(x[1])))
+
+    const childrenString = groups.map(g => {
+      const groupString = recursive(g[1])
+
+      if (groupString) {
+        const indented = groupString.split('\n').map(l => '| ' + l).join('\n')
+        return '\n' + g[0] + '\n' + indented
+      } else {
+        return g[0]
+      }
+    }).join('\n')
+
+    const tracksString = (showTracks ? nonGroups.map(g => g[0]).join('\n') : '')
+
+    if (tracksString && childrenString) {
+      return tracksString + '\n' + childrenString
+    } else if (childrenString) {
+      return childrenString
+    } else if (tracksString) {
+      return tracksString
+    } else {
+      return ''
+    }
+  }
+
+  return recursive(playlist)
+}
+
+function parsePathString(pathString) {
+  const pathParts = pathString.split('/')
+  return pathParts
+}
+
+module.exports = {
+  flattenPlaylist,
+  filterPlaylistByPathString, filterPlaylistByPath,
+  ignoreGroupByPathString, ignoreGroupByPath,
+  parsePathString
+}
diff --git a/src/process-argv.js b/src/process-argv.js
new file mode 100644
index 0000000..3193d98
--- /dev/null
+++ b/src/process-argv.js
@@ -0,0 +1,30 @@
+'use strict'
+
+module.exports = async function processArgv(argv, handlers) {
+  let i = 0
+
+  async function handleOpt(opt) {
+    if (opt in handlers) {
+      await handlers[opt]({
+        argv, index: i,
+        nextArg: function() {
+          i++
+          return argv[i]
+        },
+        alias: function(optionToRun) {
+          handleOpt(optionToRun)
+        }
+      })
+    } else {
+      console.warn('Option not understood: ' + opt)
+    }
+  }
+
+  for (; i < argv.length; i++) {
+    const cur = argv[i]
+    if (cur.startsWith('-')) {
+      const opt = cur.slice(1)
+      await handleOpt(opt)
+    }
+  }
+}
diff --git a/src/promisify-process.js b/src/promisify-process.js
new file mode 100644
index 0000000..877cb8d
--- /dev/null
+++ b/src/promisify-process.js
@@ -0,0 +1,19 @@
+'use strict'
+
+module.exports = function promisifyProcess(proc, showLogging = true) {
+  return new Promise((resolve, reject) => {
+    if (showLogging) {
+      proc.stdout.pipe(process.stdout)
+      proc.stderr.pipe(process.stderr)
+    }
+
+    proc.on('exit', code => {
+      if (code === 0) {
+        resolve()
+      } else {
+        console.error('Process failed!', proc.spawnargs)
+        reject(code)
+      }
+    })
+  })
+}
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 0000000..324df83
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,65 @@
+TODO: Get `avconv` working. Oftentimes `play` won't be able to play
+      some tracks due to an unsupported format; we'll need to use
+      `avconv` to convert them (to WAV).
+      (Done!)
+
+TODO: Get `play` working.
+      (Done!)
+
+TODO: Get play-next working; probably just act like a shuffle. Will
+      need to keep an eye out for the `play` process finishing.
+      (Done!)
+
+TODO: Preemptively download and process the next track, while the
+      current one is playing, to eliminate the silent time between
+      tracks.
+      (Done!)
+
+TODO: Delete old tracks! Since we aren't overwriting files, we
+      need to manually delete files once we're done with them.
+      (Done!)
+
+TODO: Clean up on SIGINT.
+
+TODO: Get library filter path from stdin.
+      (Done!)
+
+TODO: Show library tree. Do this AFTER filtering, so that people
+      can e.g. see all albums by a specific artist.
+      (Done!)
+
+TODO: Ignore .DS_Store.
+      (Done!)
+
+TODO: Have a download timeout, somehow.
+
+TODO: Fix the actual group format. Often times we get single-letter
+      files being downloaded (which don't exist); I'm guessing that's
+      related to folder names (which are just strings, not title-href
+      arrays) still being in the group array. (Update: that's defin-
+      itely true; 'Saucey Sounds'[0] === 'S', and 'Unofficial'[0]
+      === 'U', which are the two "files" it crashes on while playing
+      -g 'Jake Chudnow'.)
+      (Done!)
+
+TODO: A way to exclude a specific group path.
+      (Done!)
+
+TODO: Better argv handling.
+      (Done!)
+
+TODO: Option to include a specific path from the source playlist.
+      (Done!)
+
+TODO: Make a playlist generator that parses http://billwurtz.com
+      instrumentals.html.
+      (Done!)
+
+TODO: Make crawl-itunes.js a bit more general, more command-line
+      friendly (i.e. don't require editing the script itself), and
+      make it use the getHTMLLinks function defined in the new
+      crawl-links.js script.
+      (Done!)
+
+TODO: Play-in-order track picker.
+      (Done!)