├── 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
--------------------------------------------------------------------------------