diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | bill-everything.json | 754 | ||||
-rw-r--r-- | bill-instrumentals.json | 182 | ||||
-rw-r--r-- | play.js | 457 | ||||
-rw-r--r-- | src/loop-play.js | 69 | ||||
-rw-r--r-- | src/pickers.js | 33 | ||||
-rw-r--r-- | src/play.js | 159 | ||||
-rw-r--r-- | src/playlist-utils.js | 109 | ||||
-rw-r--r-- | src/process-argv.js | 30 | ||||
-rw-r--r-- | src/promisify-process.js | 19 | ||||
-rw-r--r-- | todo.txt | 65 |
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!) |