├── .gitignore ├── Advent of Code Downloader.sublime-project ├── aocdl ├── .gitignore ├── config.go └── main.go ├── build.go ├── go.mod ├── license.txt └── readme.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | 3 | *.sublime-workspace 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Advent of Code Downloader.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /aocdl/.gitignore: -------------------------------------------------------------------------------- 1 | /aocdl 2 | /aocdl.exe 3 | -------------------------------------------------------------------------------- /aocdl/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | ) 10 | 11 | type configuration struct { 12 | SessionCookie string `json:"session-cookie"` 13 | Output string `json:"output"` 14 | Year int `json:"year"` 15 | Day int `json:"day"` 16 | Force bool `json:"-"` 17 | Wait bool `json:"-"` 18 | } 19 | 20 | func loadConfigs() (*configuration, error) { 21 | config := new(configuration) 22 | 23 | home := "" 24 | usr, err := user.Current() 25 | if err == nil { 26 | home = usr.HomeDir 27 | } 28 | 29 | if home != "" { 30 | err = config.mergeWithFileIfExists(filepath.Join(home, ".aocdlconfig")) 31 | if err != nil { 32 | return nil, err 33 | } 34 | } 35 | 36 | wd, _ := os.Getwd() 37 | 38 | // If we could not determine either directory or if we are not currently in 39 | // the home directory, try and load the configuration relative to the 40 | // current working directory. 41 | if wd == "" || home == "" || wd != home { 42 | err = config.mergeWithFileIfExists(".aocdlconfig") 43 | if err != nil { 44 | return nil, err 45 | } 46 | } 47 | 48 | return config, nil 49 | } 50 | 51 | func loadConfig(filename string) (*configuration, error) { 52 | data, err := ioutil.ReadFile(filename) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | config := new(configuration) 58 | err = json.Unmarshal(data, config) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return config, nil 64 | } 65 | 66 | func (config *configuration) mergeWithFileIfExists(filename string) error { 67 | loaded, err := loadConfig(filename) 68 | if err == nil { 69 | // file loaded 70 | config.merge(loaded) 71 | return nil 72 | } else if os.IsNotExist(err) { 73 | // file not found 74 | return nil 75 | } else { 76 | // read error 77 | return err 78 | } 79 | } 80 | 81 | func (config *configuration) merge(other *configuration) { 82 | if other.SessionCookie != "" { 83 | config.SessionCookie = other.SessionCookie 84 | } 85 | if other.Output != "" { 86 | config.Output = other.Output 87 | } 88 | if other.Year != 0 { 89 | config.Year = other.Year 90 | } 91 | if other.Day != 0 { 92 | config.Day = other.Day 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /aocdl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "math/rand" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | "text/template" 14 | "time" 15 | 16 | _ "time/tzdata" 17 | ) 18 | 19 | const titleAboutMessage = `Advent of Code Downloader 20 | 21 | aocdl is a command line utility that automatically downloads your Advent of Code 22 | puzzle inputs. 23 | ` 24 | 25 | const usageMessage = `Usage: 26 | 27 | aocdl [options] 28 | 29 | Options: 30 | 31 | -session-cookie 0123456789...abcdef 32 | Use the specified string as session cookie. 33 | 34 | -output input.txt 35 | Save the downloaded puzzle input to the specified file. The special 36 | markers {{.Year}} and {{.Day}} will be replaced with the selected year 37 | and day. [see also Go documentation for text/template] 38 | 39 | -year 2000 40 | -day 24 41 | Download the input from the specified year or day. By default the 42 | current year and day is used. 43 | 44 | -force 45 | Overwrite file if it already exists. 46 | 47 | -wait 48 | If this flag is specified, year and day are ignored and the program 49 | waits until midnight (when new puzzles are released) and then downloads 50 | the input of the new day. While waiting a countdown is displayed. To 51 | reduce load on the Advent of Code servers, the download is started after 52 | a random delay between 2 and 30 seconds after midnight. 53 | ` 54 | 55 | const repositoryMessage = `Repository: 56 | 57 | https://github.com/GreenLightning/advent-of-code-downloader 58 | ` 59 | 60 | const missingSessionCookieMessage = `No Session Cookie 61 | 62 | A session cookie is required to download your personalized puzzle input. 63 | 64 | Please provide your session cookie as a command line parameter: 65 | 66 | aocdl -session-cookie 0123456789...abcdef 67 | 68 | Or create a configuration file named '.aocdlconfig' in your home directory or in 69 | the current directory and add the 'session-cookie' key: 70 | 71 | { 72 | "session-cookie": "0123456789...abcdef" 73 | } 74 | ` 75 | 76 | func main() { 77 | rand.Seed(time.Now().Unix()) 78 | 79 | config, err := loadConfigs() 80 | checkError(err) 81 | 82 | addFlags(config) 83 | 84 | if config.SessionCookie == "" { 85 | fmt.Fprintln(os.Stderr, missingSessionCookieMessage) 86 | os.Exit(1) 87 | } 88 | 89 | est, err := time.LoadLocation("EST") 90 | if err != nil { 91 | fmt.Fprintln(os.Stderr, "failed to load time zone information:", err) 92 | os.Exit(1) 93 | } 94 | 95 | now := time.Now().In(est) 96 | next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, est) 97 | 98 | if config.Year == 0 { 99 | config.Year = now.Year() 100 | } 101 | if config.Day == 0 { 102 | config.Day = now.Day() 103 | } 104 | if config.Output == "" { 105 | config.Output = "input.txt" 106 | } 107 | 108 | if config.Wait { 109 | // Overwrite values before rendering output. 110 | config.Year = next.Year() 111 | config.Day = next.Day() 112 | } 113 | 114 | err = renderOutput(config) 115 | checkError(err) 116 | 117 | // Check if output file exists before waiting and before downloading. 118 | info, err := os.Stat(config.Output) 119 | if err == nil { 120 | if info.IsDir() { 121 | fmt.Fprintf(os.Stderr, "cannot write to '%s' because it is a directory\n", config.Output) 122 | os.Exit(1) 123 | } 124 | if !config.Force { 125 | fmt.Fprintf(os.Stderr, "file '%s' already exists; use '-force' to overwrite\n", config.Output) 126 | os.Exit(1) 127 | } 128 | } else if !os.IsNotExist(err) { 129 | fmt.Fprintf(os.Stderr, "failed to check output file '%s': %v\n", config.Output, err) 130 | os.Exit(1) 131 | } 132 | 133 | if config.Wait { 134 | wait(next) 135 | } 136 | 137 | err = download(config) 138 | checkError(err) 139 | } 140 | 141 | func checkError(err error) { 142 | if err != nil { 143 | fmt.Fprintln(os.Stderr, err) 144 | os.Exit(1) 145 | } 146 | } 147 | 148 | func addFlags(config *configuration) { 149 | flags := flag.NewFlagSet("", flag.ContinueOnError) 150 | 151 | ignored := new(bytes.Buffer) 152 | flags.SetOutput(ignored) 153 | 154 | sessionCookieFlag := flags.String("session-cookie", "", "") 155 | outputFlag := flags.String("output", "", "") 156 | yearFlag := flags.String("year", "", "") 157 | dayFlag := flags.String("day", "", "") 158 | 159 | forceFlag := flags.Bool("force", false, "") 160 | waitFlag := flags.Bool("wait", false, "") 161 | 162 | var year, day int 163 | 164 | flagErr := flags.Parse(os.Args[1:]) 165 | 166 | if flagErr == nil { 167 | year, flagErr = parseIntFlag(*yearFlag) 168 | } 169 | 170 | if flagErr == nil { 171 | day, flagErr = parseIntFlag(*dayFlag) 172 | } 173 | 174 | if flagErr == flag.ErrHelp { 175 | fmt.Println(titleAboutMessage) 176 | fmt.Println(usageMessage) 177 | fmt.Println(repositoryMessage) 178 | os.Exit(0) 179 | } 180 | 181 | if flagErr != nil { 182 | fmt.Fprintln(os.Stderr, flagErr) 183 | fmt.Fprintln(os.Stderr) 184 | fmt.Fprintln(os.Stderr, usageMessage) 185 | os.Exit(1) 186 | } 187 | 188 | flagConfig := new(configuration) 189 | flagConfig.SessionCookie = *sessionCookieFlag 190 | flagConfig.Output = *outputFlag 191 | flagConfig.Year = year 192 | flagConfig.Day = day 193 | 194 | config.merge(flagConfig) 195 | 196 | if *forceFlag { 197 | config.Force = true 198 | } 199 | if *waitFlag { 200 | config.Wait = true 201 | } 202 | } 203 | 204 | func parseIntFlag(text string) (int, error) { 205 | if text == "" { 206 | return 0, nil 207 | } 208 | // Parse in base 10. 209 | value, err := strconv.ParseInt(text, 10, 0) 210 | return int(value), err 211 | } 212 | 213 | func renderOutput(config *configuration) error { 214 | tmpl, err := template.New("output").Parse(config.Output) 215 | if err != nil { 216 | return err 217 | } 218 | 219 | buf := new(bytes.Buffer) 220 | 221 | data := make(map[string]int) 222 | data["Year"] = config.Year 223 | data["Day"] = config.Day 224 | 225 | err = tmpl.Execute(buf, data) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | config.Output = buf.String() 231 | 232 | return nil 233 | } 234 | 235 | func wait(next time.Time) { 236 | min, max := 2*1000, 30*1000 237 | delayMillis := min + rand.Intn(max-min+1) 238 | 239 | hours, mins, secs := 0, 0, 0 240 | for remaining := time.Until(next); remaining >= 0; remaining = time.Until(next) { 241 | remaining += 1 * time.Second // let casts round up instead of down 242 | newHours := int(remaining.Hours()) % 24 243 | newMins := int(remaining.Minutes()) % 60 244 | newSecs := int(remaining.Seconds()) % 60 245 | if newHours != hours || newMins != mins || newSecs != secs { 246 | hours, mins, secs = newHours, newMins, newSecs 247 | fmt.Printf("\r%02d:%02d:%02d + %04.1fs", hours, mins, secs, float32(delayMillis)/1000.0) 248 | } 249 | time.Sleep(200 * time.Millisecond) 250 | } 251 | 252 | next = next.Add(time.Duration(delayMillis) * time.Millisecond) 253 | 254 | millis := 0 255 | for remaining := time.Until(next); remaining >= 0; remaining = time.Until(next) { 256 | newMillis := int(remaining.Nanoseconds() / 1e6) 257 | if newMillis != millis { 258 | millis = newMillis 259 | fmt.Printf("\r00:00:00 + %04.1fs", float32(millis)/1000.0) 260 | } 261 | time.Sleep(20 * time.Millisecond) 262 | } 263 | 264 | fmt.Printf("\r \r") 265 | } 266 | 267 | func download(config *configuration) error { 268 | client := new(http.Client) 269 | 270 | req, err := http.NewRequest("GET", fmt.Sprintf("https://adventofcode.com/%d/day/%d/input", config.Year, config.Day), nil) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | req.Header.Set("User-Agent", "github.com/GreenLightning/advent-of-code-downloader") 276 | 277 | cookie := new(http.Cookie) 278 | cookie.Name, cookie.Value = "session", config.SessionCookie 279 | req.AddCookie(cookie) 280 | 281 | resp, err := client.Do(req) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | defer resp.Body.Close() 287 | 288 | if resp.StatusCode != 200 { 289 | return errors.New(resp.Status) 290 | } 291 | 292 | flags := os.O_WRONLY | os.O_CREATE 293 | if config.Force { 294 | flags |= os.O_TRUNC 295 | } else { 296 | flags |= os.O_EXCL 297 | } 298 | 299 | file, err := os.OpenFile(config.Output, flags, 0666) 300 | if os.IsExist(err) { 301 | return fmt.Errorf("file '%s' already exists; use '-force' to overwrite", config.Output) 302 | } else if err != nil { 303 | return err 304 | } 305 | 306 | defer file.Close() 307 | 308 | _, err = io.Copy(file, resp.Body) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | return nil 314 | } 315 | -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | // This is a build script that produces the assets for a release. 12 | // Use the following command to execute the script: 13 | // go run build.go 14 | 15 | func main() { 16 | folderName := "build" 17 | 18 | check(os.MkdirAll(folderName, 0777)) 19 | check(os.Chdir("aocdl")) 20 | 21 | build("windows", "amd64", "", "aocdl.exe", "aocdl-windows.zip", folderName) 22 | build("darwin", "amd64", "", "aocdl", "aocdl-macos.zip", folderName) 23 | build("linux", "amd64", "", "aocdl", "aocdl-linux-amd64.zip", folderName) 24 | build("linux", "arm", "6", "aocdl", "aocdl-armv6.zip", folderName) 25 | } 26 | 27 | func build(goos, goarch, goarm string, binaryName, packageName, folderName string) { 28 | fullBinaryName := fmt.Sprintf("../%s/%s", folderName, binaryName) 29 | fullPackageName := fmt.Sprintf("../%s/%s", folderName, packageName) 30 | 31 | cmd := exec.Command("go", "build", "-o", fullBinaryName) 32 | 33 | cmd.Env = os.Environ() 34 | cmd.Env = append(cmd.Env, fmt.Sprintf("GOOS=%s", goos)) 35 | cmd.Env = append(cmd.Env, fmt.Sprintf("GOARCH=%s", goarch)) 36 | cmd.Env = append(cmd.Env, fmt.Sprintf("GOARM=%s", goarm)) 37 | 38 | check(cmd.Run()) 39 | check(createZip(fullBinaryName, fullPackageName)) 40 | check(os.Remove(fullBinaryName)) 41 | } 42 | 43 | func createZip(fullBinaryName, fullPackageName string) error { 44 | zipFile, err := os.Create(fullPackageName) 45 | if err != nil { 46 | return err 47 | } 48 | defer zipFile.Close() 49 | 50 | zipWriter := zip.NewWriter(zipFile) 51 | defer zipWriter.Close() 52 | 53 | binaryFile, err := os.Open(fullBinaryName) 54 | if err != nil { 55 | return err 56 | } 57 | defer binaryFile.Close() 58 | 59 | info, err := binaryFile.Stat() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | header, err := zip.FileInfoHeader(info) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | header.Method = zip.Deflate 70 | header.SetMode(0755) 71 | 72 | fileWriter, err := zipWriter.CreateHeader(header) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | _, err = io.Copy(fileWriter, binaryFile) 78 | return err 79 | } 80 | 81 | func check(err error) { 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GreenLightning/advent-of-code-downloader 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Green Lightning 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # Advent of Code Downloader 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/GreenLightning/advent-of-code-downloader)](https://goreportcard.com/report/github.com/GreenLightning/advent-of-code-downloader) 4 | 5 | `aocdl` is a command line utility that automatically downloads your [Advent of 6 | Code](https://adventofcode.com/) puzzle inputs. 7 | 8 | This tool is for competitive programmers, who want to solve the puzzles as 9 | fast as possible. Using the `-wait` flag you can actually start the program 10 | before the puzzle is published, thus spending exactly zero seconds of 11 | competition time on downloading the input (see [Basic Usage](#basic-usage) for 12 | details). 13 | 14 | If you are working with the command line, it might also be more comfortable to 15 | type `aocdl -year 2015 -day 1` instead of downloading the puzzle input using 16 | the browser. 17 | 18 | *Trivia*: If the puzzle input is very short, it will be embedded into the 19 | puzzle page instead of being linked (for an example see [day 4 of 20 | 2015](https://adventofcode.com/2015/day/4)). Thanks to the consistent API of 21 | the Advent of Code website, these puzzle inputs can be downloaded exactly like 22 | the normal, longer puzzle inputs. 23 | 24 | ## Installation 25 | 26 | #### Binary Download 27 | 28 | You can download pre-compiled binaries from the 29 | [releases](https://github.com/GreenLightning/advent-of-code-downloader/releases/latest/) 30 | page. Just unzip the archive and place the binary in your working directory or 31 | in a convenient location in your PATH. 32 | 33 | #### Build From Source 34 | 35 | If you have the [Go](https://golang.org/) compiler installed, you can use the 36 | standard `go install` command to download, build and install `aocdl`. 37 | 38 | ``` 39 | go install github.com/GreenLightning/advent-of-code-downloader/aocdl@latest 40 | ``` 41 | 42 | ## Setting the Session Cookie 43 | 44 | Your session cookie is required to download your personalized puzzle input. 45 | See the two sections below, if you want to know what a session cookie is or 46 | how to get yours. The session cookies from the Advent of Code website are 47 | valid for about a month, so you only have to get your cookie once per event. 48 | You can provide it to `aocdl` in two ways. 49 | 50 | Set your session cookie as a command line parameter: 51 | 52 | ``` 53 | aocdl -session-cookie 0123456789...abcdef 54 | ``` 55 | 56 | Or create a configuration file named `.aocdlconfig` in your home directory or in 57 | the current directory and add the `session-cookie` key: 58 | 59 | ```json 60 | { 61 | "session-cookie": "0123456789...abcdef" 62 | } 63 | ``` 64 | 65 | You can test your setup by downloading an old puzzle input using `aocdl -year 66 | 2020 -day 1`. You will get an appropriate error message if the program cannot 67 | find a session cookie. However, if you get a `500 Internal Server Error`, this 68 | most likely means your session cookie is invalid or expired. 69 | 70 | #### What Is a Session Cookie? 71 | 72 | A session cookie is a small piece of data used to authenticate yourself to the 73 | Advent of Code web servers. It is not human-readable and might look something 74 | like this (this is not a valid cookie): 75 | 76 | ``` 77 | 53616c7465645f5fbd2d445187c5dc5463efb7020021c273c3d604b5946f9e87e2dc30b649f9b2235e8cd57632e415cb 78 | ``` 79 | 80 | When you log in, the Advent of Code server generates a new session cookie and 81 | sends it to your browser, which saves it on your computer. Every time you make 82 | a request, your browser sends the cookie back to the server, which is how the 83 | server knows that the request is from you and not somebody else. That way the 84 | server can send you a personalized version of the website (for example 85 | displaying your username and current number of stars or sending you your 86 | personal puzzle input instead of somebody else's input). 87 | 88 | #### How Do I Get My Session Cookie? 89 | 90 | Google Chrome: 91 | 92 | - Go to [adventofcode.com](https://adventofcode.com/) 93 | - Make sure you are logged in 94 | - Right click and select "Inspect" 95 | - Select the "Application" tab 96 | - In the tree on the left, select "Storage" → "Cookies" → "https://adventofcode.com" 97 | - You should see a table of cookies, find the row with "session" as name 98 | - Double click the row in the "Value" column to select the value of the cookie 99 | - Press `CTRL + C` or right click and select "Copy" to copy the cookie 100 | - Paste it into your configuration file or on the command line 101 | 102 | Mozilla Firefox: 103 | 104 | - Go to [adventofcode.com](https://adventofcode.com/) 105 | - Make sure you are logged in 106 | - Right click and select "Inspect Element" 107 | - Select the "Storage" tab 108 | - In the tree on the left, select "Cookies" → "https://adventofcode.com" 109 | - You should see a table of cookies, find the row with "session" as name 110 | - Double click the row in the "Value" column to select the value of the cookie 111 | - Press `CTRL + C` or right click and select "Copy" to copy the cookie 112 | - Paste it into your configuration file or on the command line 113 | 114 | ## Basic Usage 115 | 116 | Assuming you have created a configuration file (if not you must provide your 117 | session cookie as a parameter), the following command will attempt to download 118 | the input for the current day and save it to a file named `input.txt`: 119 | 120 | ``` 121 | aocdl 122 | ``` 123 | 124 | If you specify the `-wait` flag, the program will display a countdown waiting 125 | for midnight (when new puzzles are released) and then download the input of 126 | the new day: 127 | 128 | ``` 129 | aocdl -wait 130 | ``` 131 | 132 | Finally, you can also specify a day (and year) explicitly. 133 | 134 | ``` 135 | aocdl -day 1 136 | aocdl -year 2015 -day 1 137 | ``` 138 | 139 | ## Options 140 | 141 | -session-cookie 0123456789...abcdef 142 | Use the specified string as session cookie. 143 | 144 | -output input.txt 145 | Save the downloaded puzzle input to the specified file. The special 146 | markers {{.Year}} and {{.Day}} will be replaced with the selected year 147 | and day. [see also Go documentation for text/template] 148 | 149 | -year 2000 150 | -day 24 151 | Download the input from the specified year or day. By default the 152 | current year and day is used. 153 | 154 | -force 155 | Overwrite file if it already exists. 156 | 157 | -wait 158 | If this flag is specified, year and day are ignored and the program 159 | waits until midnight (when new puzzles are released) and then downloads 160 | the input of the new day. While waiting a countdown is displayed. To 161 | reduce load on the Advent of Code servers, the download is started after 162 | a random delay between 2 and 30 seconds after midnight. 163 | 164 | ## Configuration Files 165 | 166 | The program looks for configuration files named `.aocdlconfig` in the user's 167 | home directory and in the current working directory. 168 | 169 | For each option, the configuration file in the current directory overwrites the 170 | configuration file in the home directory and command line parameters overwrite 171 | any configuration file. 172 | 173 | Configuration files must contain one valid JSON object. The following keys 174 | corresponding to some of the command line parameters above are accepted: 175 | 176 | | Key | Type | 177 | | ---------------- | ------ | 178 | | "session-cookie" | String | 179 | | "output" | String | 180 | | "year" | Number | 181 | | "day" | Number | 182 | 183 | A fully customized configuration file might look like this, although the program 184 | would only ever download the same input unless the date is specified on the 185 | command line: 186 | 187 | ```json 188 | { 189 | "session-cookie": "0123456789...abcdef", 190 | "output": "input-{{.Year}}-{{.Day}}.txt", 191 | "year": 2015, 192 | "day": 24 193 | } 194 | ``` 195 | --------------------------------------------------------------------------------