├── .gitignore ├── myfitnesspal ├── main_test.go ├── main.go └── diary.go ├── client.go ├── client_test.go ├── types.go ├── login.go ├── README.md ├── food_diary_test.go └── food_diary.go /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore binaries 2 | /myfitnesspal/myfitnesspal 3 | 4 | # ignore temp files 5 | /*.env 6 | 7 | -------------------------------------------------------------------------------- /myfitnesspal/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestCompiles(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package myfitnesspal 2 | 3 | import "net/http" 4 | 5 | const ( 6 | Codebase = "https://www.myfitnesspal.com/" 7 | LoginUrl = Codebase + "account/login" 8 | FoodDiaryUrl = Codebase + "food/diary/" 9 | ) 10 | 11 | type Client struct { 12 | client *http.Client 13 | username string 14 | } 15 | 16 | func New(username, password string) (*Client, error) { 17 | client, err := login(username, password) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return &Client{ 23 | client: client, 24 | username: username, 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package myfitnesspal 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestDateFormat(t *testing.T) { 12 | Convey("Given a time.Time instance", t, func() { 13 | now := time.Date(2014, 4, 5, 6, 0, 0, 0, time.Local) 14 | 15 | Convey("When I call #ToDate", func() { 16 | dateString := now.Format("2006-01-02") 17 | 18 | Convey("Then I expect a well formatted date string", func() { 19 | So(dateString, ShouldEqual, "2014-04-05") 20 | }) 21 | }) 22 | }) 23 | } 24 | 25 | func TestItoa(t *testing.T) { 26 | Convey("Given a number => 1,234", t, func() { 27 | i := 1234 28 | 29 | Convey("When I call #Itoa", func() { 30 | str := strconv.Itoa(i) 31 | 32 | Convey("When I expect the number to have been parsed successfully", func() { 33 | So(str, ShouldEqual, "1234") 34 | }) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /myfitnesspal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/codegangsta/cli" 8 | ) 9 | 10 | type Options struct { 11 | Username string 12 | Password string 13 | } 14 | 15 | func Opts(c *cli.Context) *Options { 16 | return &Options{ 17 | Username: c.String("username"), 18 | Password: c.String("password"), 19 | } 20 | } 21 | 22 | var flags = []cli.Flag{ 23 | cli.StringFlag{"username", "", "myfitnesspal username", "MYFITNESSPAL_USERNAME"}, 24 | cli.StringFlag{"password", "", "myfitnesspal password", "MYFITNESSPAL_PASSWORD"}, 25 | } 26 | 27 | func main() { 28 | app := cli.NewApp() 29 | app.Name = "myfitnesspal" 30 | app.Usage = "cli interface to myfitnesspal" 31 | app.Author = "Matt Ho" 32 | app.Commands = []cli.Command{ 33 | diaryCommand, 34 | } 35 | app.Run(os.Args) 36 | } 37 | 38 | func check(err error) { 39 | if err != nil { 40 | log.Fatalln(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /myfitnesspal/diary.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/codegangsta/cli" 9 | "github.com/savaki/myfitnesspal" 10 | ) 11 | 12 | var diaryCommand = cli.Command{ 13 | Name: "food-diary", 14 | Usage: "retrieve food diary information", 15 | Flags: append(flags, []cli.Flag{ 16 | cli.StringFlag{"date", time.Now().Format(myfitnesspal.DateFormat), "the date to retrieve YYYY-MM-DD e.g. 2015-02-01", ""}, 17 | }...), 18 | Action: diaryAction, 19 | } 20 | 21 | func diaryAction(c *cli.Context) { 22 | opts := Opts(c) 23 | 24 | client, err := myfitnesspal.New(opts.Username, opts.Password) 25 | check(err) 26 | 27 | timeString := c.String("date") 28 | date, err := time.Parse(myfitnesspal.DateFormat, timeString) 29 | check(err) 30 | 31 | entry, err := client.FoodDiary(date) 32 | check(err) 33 | 34 | data, err := json.MarshalIndent(entry, "", " ") 35 | check(err) 36 | 37 | fmt.Println(string(data)) 38 | } 39 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package myfitnesspal 2 | 3 | import "fmt" 4 | 5 | const ( 6 | DateFormat = "2006-01-02" 7 | ) 8 | 9 | var ( 10 | ErrNotLoggedIn = fmt.Errorf("not logged in") 11 | ) 12 | 13 | type Macros struct { 14 | Section string `json:"-"` 15 | Label string `json:"label"` 16 | Calories int `json:"calories"` 17 | Carbs int `json:"carbs"` 18 | Fat int `json:"fat"` 19 | Protein int `json:"protein"` 20 | Sodium int `json:"sodium"` 21 | Sugar int `json:"sugar"` 22 | } 23 | 24 | type DiaryEntry struct { 25 | Breakfast MacrosArray `json:"breakfast,omitempty"` 26 | Lunch MacrosArray `json:"lunch,omitempty"` 27 | Dinner MacrosArray `json:"dinner,omitempty"` 28 | Snacks MacrosArray `json:"snacks,omitempty"` 29 | 30 | Totals *Macros `json:"totals"` 31 | Goal *Macros `json:"goal"` 32 | Remaining *Macros `json:"remaining"` 33 | } 34 | 35 | type MacrosArray []*Macros 36 | 37 | func (m MacrosArray) Totals() *Macros { 38 | macros := &Macros{} 39 | 40 | if m == nil || len(m) == 0 { 41 | return macros 42 | } 43 | 44 | macros.Section = m[0].Section 45 | macros.Label = m[0].Section + " Totals" 46 | for _, item := range m { 47 | macros.Calories = macros.Calories + item.Calories 48 | macros.Carbs = macros.Carbs + item.Carbs 49 | macros.Fat = macros.Fat + item.Fat 50 | macros.Protein = macros.Protein + item.Protein 51 | macros.Sodium = macros.Sodium + item.Sodium 52 | macros.Sugar = macros.Sugar + item.Sugar 53 | } 54 | 55 | return macros 56 | } 57 | 58 | func (m MacrosArray) Find(section, label string) *Macros { 59 | items := m.FindAll(section) 60 | 61 | for _, item := range items { 62 | if item.Label == label { 63 | return item 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (m MacrosArray) FindAll(section string) MacrosArray { 71 | results := MacrosArray{} 72 | 73 | for _, item := range m { 74 | if item.Section == section { 75 | results = append(results, item) 76 | } 77 | } 78 | 79 | return results 80 | } 81 | -------------------------------------------------------------------------------- /login.go: -------------------------------------------------------------------------------- 1 | package myfitnesspal 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/cookiejar" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/PuerkitoBio/goquery" 11 | "golang.org/x/net/publicsuffix" 12 | ) 13 | 14 | func login(username, password string) (*http.Client, error) { 15 | client, err := newHttpClient() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | resp, err := client.Get(LoginUrl) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer resp.Body.Close() 25 | 26 | doc, err := goquery.NewDocumentFromReader(resp.Body) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | var authenticityToken string 32 | var utf8 string 33 | 34 | doc.Find("input[name=authenticity_token]").First().Each(func(i int, s *goquery.Selection) { 35 | authenticityToken, _ = s.Attr("value") 36 | }) 37 | 38 | doc.Find("input[name=utf8]").First().Each(func(i int, s *goquery.Selection) { 39 | utf8, _ = s.Attr("value") 40 | }) 41 | 42 | params := url.Values{} 43 | params.Set("utf8", utf8) 44 | params.Set("authenticity_token", authenticityToken) 45 | params.Set("username", username) 46 | params.Set("password", password) 47 | params.Set("remember_me", "1") 48 | resp, err = client.Post(LoginUrl, "application/x-www-form-urlencoded", strings.NewReader(params.Encode())) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer resp.Body.Close() 53 | 54 | if !isLoggedIn(resp.Body) { 55 | return nil, ErrNotLoggedIn 56 | } 57 | 58 | return client, nil 59 | } 60 | 61 | func isLoggedIn(r io.Reader) bool { 62 | doc, err := goquery.NewDocumentFromReader(r) 63 | if err != nil { 64 | return false 65 | } 66 | 67 | var loggedIn bool 68 | doc.Find(".user-2").Each(func(i int, s *goquery.Selection) { 69 | title, _ := s.Attr("title") 70 | loggedIn = len(title) > 0 71 | }) 72 | 73 | return loggedIn 74 | } 75 | 76 | func newHttpClient() (*http.Client, error) { 77 | options := cookiejar.Options{ 78 | PublicSuffixList: publicsuffix.List, 79 | } 80 | jar, err := cookiejar.New(&options) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return &http.Client{Jar: jar}, nil 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # myfitnesspal 2 | 3 | [](https://godoc.org/github.com/savaki/myfitnesspal) 4 | 5 | [](https://snap-ci.com/savaki/myfitnesspal/branch/master) 6 | 7 | A client library and command line tool for myfitnesspal. 8 | 9 | ## Installation 10 | 11 | ``` 12 | go get github.com/savaki/myfitnesspal/myfitnesspal 13 | ``` 14 | 15 | ## Command Line Usage 16 | 17 | In addition to being a go library, myfitnesspal can be used from the command line to retrieve data from myfitnesspal.com in json format. 18 | 19 | ### MyFitnessPal Authentication 20 | 21 | All myfitnesspal command line requests need to be authenticated. You can either authenticate by tacking on ```--username``` and ``--password`` to each request as follows: 22 | 23 | ``` 24 | myfitnesspal food-diary --username YOUR-USERNAME --password YOUR_PASSWORD 25 | -- 26 | ``` 27 | 28 | Or by setting environment variables: 29 | 30 | ``` 31 | export MYFITNESSPAL_USERNAME=YOUR-USERNAME 32 | export MYFITNESSPAL_PASSWORD=YOUR-PASSWORD 33 | ``` 34 | 35 | ## Food Diary 36 | 37 | ### Retrieve my macro intake for today 38 | 39 | Assuming you've set your username and password in the environment (see MyFitnessPal Authentication above). 40 | 41 | ``` 42 | myfitnesspal food-diary 43 | ``` 44 | 45 | Will return something like: 46 | 47 | ``` 48 | { 49 | "breakfast": [ 50 | { 51 | "label": "Breakfast Burrito", 52 | "calories": 450, 53 | "carbs": 250, 54 | "fat": 150, 55 | "protein": 50, 56 | "sodium": 600, 57 | "sugar": 0 58 | } 59 | ], 60 | "totals": { 61 | "label": "Totals", 62 | "calories": 450, 63 | "carbs": 250, 64 | "fat": 150, 65 | "protein": 50, 66 | "sodium": 600, 67 | "sugar": 0 68 | }, 69 | "goal": { 70 | "label": "Your Daily Goal", 71 | "calories": 450, 72 | "carbs": 250, 73 | "fat": 150, 74 | "protein": 50, 75 | "sodium": 600, 76 | "sugar": 0 77 | }, 78 | "remaining": { 79 | "label": "Remaining", 80 | "calories": 0, 81 | "carbs": 0, 82 | "fat": 0, 83 | "protein": 0, 84 | "sodium": 0, 85 | "sugar": 0 86 | } 87 | } 88 | ``` 89 | 90 | ### Retrieve my macro intake for a specific date 91 | 92 | To retrieve my intake on for a specific date, like ```Feb 1st, 2015```, I can type: 93 | 94 | ``` 95 | myfitnesspal food-diary --date 2015-02-01 96 | ``` 97 | 98 | ## Future Plans 99 | 100 | * Looking for a feature, but don't see it? Ping me and I'll see about adding it. 101 | 102 | -------------------------------------------------------------------------------- /food_diary_test.go: -------------------------------------------------------------------------------- 1 | package myfitnesspal 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestFoodDiary(t *testing.T) { 11 | Convey("Given the food diary page", t, func() { 12 | html := ` 13 |
| Totals | 16 |100 | 17 |200 | 18 |300 | 19 |400 | 20 |500 | 21 |600 | 22 |23 | |
| Your Daily Goal | 26 |27 | 1,234 | 28 |29 | 2,345 | 30 |31 | 3,456 | 32 |33 | 4,567 | 34 |35 | 5,678 | 36 |37 | 6,789 | 38 |39 | |
| Remaining | 42 |11 | 43 |22 | 44 |33 | 45 |44 | 46 |55 | 47 |66 | 48 |49 | |