├── client_secret.json ├── README.md └── massif.py /client_secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "massif-239013", 4 | "private_key_id": "6ac40002683edd42208acfed5fe343aae11b59e7", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNFBbJRmRmxHY+\nko2wdvGGeThLT8lwD9kODEqnVNyf+GJYWGmY57y7h7AmRvtrZY+2nCZRp+7N3rv1\nHrezmNckBlHk7+8ZfKmSUHpmBFPhiQksAZKyoiUy4T7ITT3eYOYjc818MT9euPX9\nIU1zNSC+dxyneIpEhNacuGkxkvFSsZeFdUo8uemOlgUFhN4ehH7YXuGm+xUAl/TQ\neoaFzz4p+iEXyd4EQ1l1S+NHzO2DRuFbdT6ovvW4y8TOV4fmOLn/4LLs3qivGZ9V\n5qt0V8tnbAsOrOA8QYkKNfe7Vq3fAr5cd831h3waML4Uzyr5qAji2cUuWNy+YRwv\ncXFJ5s2XAgMBAAECggEAUqecH3ddlAXu2n1a2hq2ccp9o+z+dUoN45mUpSiQ23DS\nwmSv+s67xKGFn4fQYZLHf4Qj9ZlHqHXzL3I3/AzH+V8Ktsj1h0I1XiFNk7x+ylHe\n/nuL0q+DaqRaF4T6QJyxZOyRj1KSBe1qR6Gag6qQDfQX+m+c2sznarbwvuhIl1j+\n/DpRaETieP1dgBM8ZJix3g7pgp3BkHts9ndBmITqSm73ztQvXDlnxuNGRvVWwdzd\nLHlr8ePIyURofxYgmtQ5NMBnyFtdKcoNMl0V5KGTTE3Kzkt9gR9KvbjN4+VqOvk2\n7XP/1Qw+5+2q4hx/OO/42io0Qy5RTMt78FXchk3dsQKBgQD8Fw2uSJ6eeaGXMtH1\n+vXcr8RPQ2uyqe7W/dIphjG/Bp7X0KAi6VvD/eFY1bveHXCGszrM0FxIMoJXH/7H\nE+VY4X5VAM9kRXDXN6OwulW8M0IWw810JO7aX+2DcGL2PbUaPtKE01dSTZ63sgYH\nUKPz1x8+Vaq6e/GNf30q8h+FZQKBgQDQQl8uS2NunQKh6ZB/CtUboPMLBJtNp2JL\nQl0TmEGwEZu8DPQ19Guu+Pdj/HNHqCwAH1rA0ip6YVlxDwa+co/47Ax0DQUnU3j9\nzpKa4IxD/JSSseTe+N0QRX4NyQK9CMxLfLufYXSlbSiR6V9ZiGuhuyjukwdy9ljo\n/175NrTFSwKBgQCdQ/CH+svhx7WUcuLjVuXNAGYyoLfuZO8Ydo6G0y3zozizIHbW\ncMiL07WiyuwB3FHX5rZXEeGQNNp1agNyxKm+siYy92dqgZus2Awpc4WK/FtNgmeI\n/oV3/IJbDmDeh46Uyf5hWMtQEBZlOQ1jwN4Xf+wA4ka5Qhtmj9NRWG1rtQKBgBxN\n2PfGwTXIWxI8VyodA8ekgUOvFZhhNme6FkJSgCL40aymKg6nMHdwWNca+WP0xD4k\nBMaOCb3mOyy1eRorIcwX8L1ZA6lLm/cKuzwXZpja3CpvyQZQ1mKevzoKZrfgWTut\nMSbBLQRKqKfkCtR1SQOLF04NZ4bFWmIYSwUd3UWvAoGBALbGUUHhuLPufxhISjlD\n3hhzJC/0W+MZe8jfWmgxwekYilf6+iQfx3oZVjOgfeuLNJ/JI9vnXBmIsFdu4pv4\n3OIimM0P9aPF4Bj8nBkfKc9pHzXjnLZpkKjqEsn3t4VB2v4+F10qtTroI+4kV0X/\nRKNy6uV/AY8PkpB+QgsLhnqY\n-----END PRIVATE KEY-----\n", 6 | "client_email": "massif@massif-239013.iam.gserviceaccount.com", 7 | "client_id": "114811900073104349212", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://oauth2.googleapis.com/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/massif%40massif-239013.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | massive noun
4 | /ˈmasiv/
5 | The people, the crew, lovers of jungle and drum & bass. 6 | 7 | massif noun
8 | /maˈsēf/
9 | A principal mountain mass. 10 | 11 | # About 12 | 13 | MASSIF is the first hiking calculator by and for junglists. It is a command line utility that helps you plan hikes by approximating the duration of your route and generating a playlist of jungle and drum & bass music to match. Planning a hike with MASSIF requires that you know the following: 14 | 15 | - Length of your route 16 | - Your average hiking speed 17 | - Ascent during your route 18 | - Terrain of your route 19 | - Weather conditions 20 | - Weight of your pack 21 | - Whether or not you're a junglist 22 | 23 | Based on these parameters MASSIF will use generally accepted heuristics, such as [Naismith’s Rule](https://en.wikipedia.org/wiki/Naismith%27s_rule), along with Aitken corrections for terrain and ascent to calculate an approximate duration for the hike. 24 | 25 | It will then present the option of generating an accompanying playlist. Track selections are currently based on the Top 100 Most Wanted Jungle / Drum & Bass Records Released in 1994-1995 based on [Discogs data from August 17, 2010](https://data.discogs.com). 26 | 27 | At the moment the database contains 12 hours 57 minutes 23 seconds of music. 28 | 29 |
30 |

31 | 32 | # Requirements 33 | - Python3 34 | - Network Connection (for playlist generation only) 35 | 36 | # Dependencies 37 | - youtube-dl 38 | - gspread 39 | - oauth2client 40 | 41 | # Setup 42 | 43 | Install Python3 44 | 45 | ```sh 46 | $ pip install --upgrade youtube_dl 47 | $ pip install --upgrade gspread 48 | $ pip install --upgrade oauth2client 49 | ``` 50 | 51 | # Usage 52 | 53 | ```sh 54 | $ python3 massif.py 55 | ``` 56 | 57 | Follow onscreen instructions. 58 | 59 | 60 | # To Do 61 | - Continue building up music database 62 | - Improve ID3 tagging 63 | - Improve error handling 64 | - Improve CLI UX 65 | 66 | 67 | # Who Made This? 68 | I'm [Jeremiah Johnson](http://jeremiahjohnson.rip) — electronic musician, creative technologist, and hiker. Currently designing, coding, consulting, and directing the artist residency program at [Barbarian](https://wearebarbarian.com). Previously, I’ve worked as a Data Engineer at Columbia University Medical Center, Adjunct Professor at New York University, Creative Director for an international music festival, and contributor to O'Reilly's technical books. I have a music production studio in Brooklyn where I use modular synths and drum machines alongside obsolete videogame consoles to produce under the name [𝑵𝑼𝑳𝑳𝑺𝑳𝑬𝑬𝑷](http://nullsleep.com) in a wide range of styles. During 2018, I wrote and recorded one new song every week for the entire year — you can find many of them on [my soundcloud](https://soundcloud.com/nullsleep). 69 | 70 | Twitter: [@Nullsleep](https://twitter.com/Nullsleep)
71 | Instagram: [@Nullsleep](https://instagram.com/Nullsleep) 72 | -------------------------------------------------------------------------------- /massif.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import youtube_dl 3 | import gspread 4 | from oauth2client.service_account import ServiceAccountCredentials 5 | import datetime 6 | from random import randrange 7 | import math 8 | 9 | print ("") 10 | print ("'|| ||' | .|'''.| .|'''.| '||' '||''''| ") 11 | print (" ||| ||| ||| ||.. ' ||.. ' || || . ") 12 | print (" |'|..'|| | || ''|||. ''|||. || ||''| ") 13 | print (" | '|' || .''''|. . '|| . '|| || || ") 14 | print (".|. | .||. .|. .||. |'....|' |'....|' .||. .||. v1.0") 15 | print ("") 16 | 17 | # HIKING CALCULATOR SECTION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 18 | # Capture and validate all user inputs. 19 | 20 | # ROUTE LENGTH | DEFAULT: NONE >>> 21 | while True: 22 | try: 23 | route_length = float(input("Length of route in miles: ")) 24 | except ValueError: 25 | print("Not an appropriate entry.") 26 | continue 27 | 28 | if route_length <= 0: 29 | print("Bumboclaat! Galang!") 30 | continue 31 | else: 32 | print(route_length) 33 | break 34 | 35 | 36 | # HIKING SPEED | DEFAULT: 3.0 >>> 37 | 38 | while True: 39 | try: 40 | speed = float(input("Average hiking speed in mph [3.0]: ") or "3.0") 41 | except ValueError: 42 | print("Not an appropriate entry.") 43 | continue 44 | 45 | if speed <= 0: 46 | print("Bumboclaat! Galang!") 47 | continue 48 | else: 49 | print(speed) 50 | break 51 | 52 | 53 | # ASCENT | DEFAULT: 0 >>> 54 | 55 | while True: 56 | try: 57 | ascent = int(input("Total ascent in feet [0]: ") or "0") 58 | except ValueError: 59 | print("Not an appropriate entry.") 60 | continue 61 | 62 | if ascent < 0: 63 | print("Bumboclaat! Galang!") 64 | continue 65 | else: 66 | print(ascent) 67 | # If there is any ascent on the route, add an extra hour per 2000 ft of ascent. 68 | ascent_modifier = ascent / 2000 69 | break 70 | 71 | 72 | # PACK WEIGHT | DEFAULT: A (LIGHT) >>> 73 | 74 | while True: 75 | try: 76 | pack = input("Weight of pack [a] light (b) regular (c) heavy: ") or "a" 77 | except ValueError: 78 | print("Sorry, I didn't understand that.") 79 | continue 80 | 81 | if pack.lower() not in ('a', 'b', 'c'): 82 | print("Not an appropriate choice.") 83 | continue 84 | else: 85 | # Pack was successfully parsed, and we're happy with its value. 86 | # If carrying a lightweight pack, our hiking speed is unchanged. 87 | if pack == "b": 88 | # If carrying a regular pack, decrease hiking speed by 0.25 mph 89 | speed = speed - 0.25 90 | elif pack == "c": 91 | # If carrying a heavy pack, decrease hiking speed by 0.5 mph 92 | speed = speed - 0.5 93 | # Check hiking speed, exit program if too low after pack weight calculation. 94 | if speed <= 0: 95 | print("Unable to calculate hike duration because your adjusted hiking speed was too low.") 96 | print("Try again with a faster hiking speed or lighter pack.") 97 | exit() 98 | print(pack) 99 | break # Exit the loop. 100 | 101 | 102 | # TERRAIN | DEFAULT: 0 >>> 103 | 104 | while True: 105 | try: 106 | terrain = int(input("Percent of route over challenging terrain [0]: ") or "0") 107 | except ValueError: 108 | print("Not an appropriate entry. Must be an integer between 0 and 100.") 109 | continue 110 | 111 | if terrain < 0 or terrain > 100: 112 | print("Not an appropriate entry. Must be an integer between 0 and 100.") 113 | continue 114 | else: 115 | # Terrain was successfully parsed, and we're happy with its value. 116 | # Check hiking speed, exit program if too low after terrain calculation. 117 | if terrain > 0 and (speed - 1) <= 0: 118 | print("Unable to calculate hike duration because your adjusted hiking speed was too low.") 119 | print("Try again with a faster hiking speed or a route over less challenging terrain.") 120 | exit() 121 | print(terrain) 122 | route_length_challenging = route_length * (terrain/100) 123 | route_length_regular = route_length - route_length_challenging 124 | terrain_modifier = ((route_length * (terrain/100)) / (speed - 1)) 125 | duration_of_hike = terrain_modifier + (route_length_regular / speed) + ascent_modifier 126 | break 127 | 128 | 129 | # WEATHER CONDITIONS | DEFAULT: A (GOOD) >>> 130 | 131 | while True: 132 | try: 133 | weather = input("Weather conditions during hike [a] good (b) poor (c) severe: ") or "a" 134 | except ValueError: 135 | print("Sorry, I didn't understand that.") 136 | continue 137 | 138 | if weather.lower() not in ('a', 'b', 'c'): 139 | print("Not an appropriate choice.") 140 | continue 141 | else: 142 | # Weather was successfully parsed, and we're happy with its value. 143 | # If hiking in good conditions, duration of hike is unchanged. 144 | if weather == "b": 145 | # If hiking in poor conditions, add 25% more time to the duration of the hike. 146 | weather_modifier = duration_of_hike * 0.25 147 | elif weather == "c": 148 | # If hiking in severe conditions, add 50% more time to the duration of the hike. 149 | weather_modifier = duration_of_hike * 0.5 150 | else: 151 | weather_modifier = 0 152 | print(weather) 153 | break # Exit the loop. 154 | 155 | 156 | # CRUNCH THE REMAINING NUMBERS >>> 157 | 158 | duration_of_hike = duration_of_hike + weather_modifier 159 | duration_hours = int(math.floor(duration_of_hike)) 160 | duration_minutes = int((duration_of_hike % 1) * 60) 161 | print("\nDuration of hike:", duration_hours, "hr", duration_minutes, "min", "\n") 162 | 163 | # ASK USER IF THEY WOULD LIKE GENERATE A PLAYLIST >>> 164 | 165 | while True: 166 | try: 167 | generate_playlist = input("Would you like to generate a playlist for your hike (Y/N)? ") 168 | except ValueError: 169 | print("\nBumboclaat!\n") 170 | continue 171 | 172 | if generate_playlist.lower() not in ('y', 'n'): 173 | print("Not an appropriate choice.") 174 | continue 175 | else: 176 | if generate_playlist == "n": 177 | print ("\nWi run tings, tings nuh run wi! Galang!") 178 | exit() 179 | else: 180 | break 181 | 182 | 183 | # YOUTUBE-DL SETUP SECTION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 184 | 185 | class MyLogger(object): 186 | def debug(self, msg): 187 | pass 188 | 189 | def warning(self, msg): 190 | pass 191 | 192 | def error(self, msg): 193 | print(msg) 194 | 195 | def my_hook(d): 196 | if d['status'] == 'downloading': 197 | print(d['filename'], d['_percent_str'], d['_eta_str']) 198 | elif d['status'] == 'finished': 199 | print('Done downloading, now converting ...') 200 | 201 | 202 | # PLAYLIST GENERATOR SECTION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 203 | 204 | playlist_length = datetime.timedelta() 205 | hike_duration = str(duration_hours) + ":" + str(duration_minutes).zfill(2) + ":00" 206 | FMT = '%H:%M:%S' 207 | 208 | # USE CREDS TO CREATE A CLIENT TO INTERACT WITH THE GOOGLE DRIVE API >>> 209 | 210 | scope = ['https://spreadsheets.google.com/feeds', 211 | 'https://www.googleapis.com/auth/drive'] 212 | creds = ServiceAccountCredentials.from_json_keyfile_name('client_secret.json', scope) 213 | client = gspread.authorize(creds) 214 | 215 | # FIND A WORKBOOK BY NAME AND OPEN THE FIRST SHEET >>> 216 | 217 | sheet = client.open("MASSIF - Jungle Database").sheet1 218 | 219 | # POPULATE LISTS WITH EXTRACTED TRACK URLS AND LENGTHS >>> 220 | 221 | artists_list = sheet.col_values(1) 222 | artists_list.pop(0) # drop column heading 223 | tracktitles_list = sheet.col_values(2) 224 | tracktitles_list.pop(0) # drop column heading 225 | urls_list = sheet.col_values(3) 226 | urls_list.pop(0) # drop column heading 227 | tracklengths_list = sheet.col_values(4) 228 | tracklengths_list.pop(0) # drop column heading 229 | 230 | while True: 231 | random_index = randrange(len(tracklengths_list)) 232 | item = tracklengths_list[random_index] 233 | (h, m, s) = item.split(':') 234 | d = datetime.timedelta(hours=int(h), minutes=int(m), seconds=int(s)) 235 | playlist_length +=d 236 | 237 | ydl_opts = { 238 | 'format': 'bestaudio/best', 239 | 'postprocessors': [{ 240 | 'key': 'FFmpegExtractAudio', 241 | 'preferredcodec': 'mp3', 242 | 'preferredquality': '192', 243 | }, 244 | { 245 | 'key': 'FFmpegMetadata', 246 | } 247 | ], 248 | 'logger': MyLogger(), 249 | 'progress_hooks': [my_hook], 250 | 'ignoreerrors': True, 251 | 'outtmpl' : r'music/{} - {}.%(ext)s'.format(artists_list[random_index], tracktitles_list[random_index]) 252 | } 253 | 254 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 255 | ydl.download([str(urls_list[random_index])]) 256 | tracklengths_list.pop(random_index) 257 | urls_list.pop(random_index) 258 | tracktitles_list.pop(random_index) 259 | artists_list.pop(random_index) 260 | 261 | if (datetime.datetime.strptime(hike_duration, FMT) - datetime.datetime.strptime(str(playlist_length), FMT)).days < 0: 262 | print("BABYLON SHALL FALL!") 263 | print("Hike duration: ", str(hike_duration)) 264 | print("Playlist duration: ", str(playlist_length)) 265 | break --------------------------------------------------------------------------------