Skip to content

Instantly share code, notes, and snippets.

@gwenhael-le-moine
Last active December 5, 2018 15:28
Show Gist options
  • Save gwenhael-le-moine/ae3cdba7a6f9f44a897d77c5390e61e8 to your computer and use it in GitHub Desktop.
Save gwenhael-le-moine/ae3cdba7a6f9f44a897d77c5390e61e8 to your computer and use it in GitHub Desktop.
Full backup of MyFitnessPal food diaries as JSON
#!/usr/bin/env ruby
require 'date'
require 'optparse'
require 'json'
require 'bundler/inline'
gemfile {
source 'https://rubygems.org'
gem 'mechanize'
gem 'netrc'
}
def date_to_string( date )
date.to_s.split("T").first
end
def login()
netrc = Netrc.read
mechanize_agent = Mechanize.new
login_page = mechanize_agent.get( 'https://www.myfitnesspal.com/fr/account/login' )
form = login_page.forms[0]
form.username = netrc['myfitnesspal'].login
form.password = netrc['myfitnesspal'].password
form.submit
mechanize_agent
end
def backup_day( agent, nickname, date )
day_diary = agent.get( "https://www.myfitnesspal.com/food/diary/#{nickname}?date=#{date_to_string( date )}" )
def extract_nutrients( tr )
data = tr.elements.map(&:text)
data = data[1 .. data.length - 2].map(&:strip)
.map { |td| td.strip.split("\n").first.sub(",", "").to_i }
{ calories: data[0],
carbs: data[1],
fat: data[2],
protein: data[3],
sodium: data[4],
sugar: data[5] }
end
def extract_meals( data )
meals = []
current_meal = {}
data.at( "#diary-table tbody" ).elements
.each do |element|
if element.attributes.size > 0 && element.attributes["class"].value == "meal_header"
current_meal = { name: element.at( "td.first" ).text,
items: [] }
elsif element.attributes.size > 0 && element.attributes["class"].value == "bottom"
meals << current_meal
elsif element.attributes.size > 0 && ["spacer", "total", "total alt", "total remaining"].include?( element.attributes["class"].value )
next
else
item = { name: element.at("td.first").text.strip }
current_meal[:items] << item.merge( extract_nutrients( element ) )
end
end
meals
end
{ date: date_to_string( date ),
meals: extract_meals( day_diary ),
consumed: extract_nutrients( day_diary.at( "tr.total" ) ),
goals: extract_nutrients( day_diary.at( "tr.total.alt" ) )
}
end
def backup_all( agent, nickname, from, to )
backup = (from .. to).flat_map do |date|
putc "."
backup_day( agent, nickname, date )
end
putc "\n"
backup
end
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: myfitnesspal-backup.rb [options]"
opts.on("-n", "--nickname NICKNAME", "Nickname in MyFitnessPal") do |nickname|
options[:nickname] = nickname
end
opts.on("-f", "--from DATE", "Starting date") do |from|
options[:from] = DateTime.parse( from )
end
opts.on("-t", "--to DATE", "End date") do |to|
options[:to] = DateTime.parse( to )
end
opts.on("-u", "--update", "Update existing file") do
options[:update] = true
end
opts.on("-o", "--output FILENAME", "Filename") do |filename|
options[:output] = filename
end
end.parse!
raise OptionParser::MissingArgument unless options.key?( :nickname ) && ( options.key?( :from ) || options.key?( :update ) )
if options[:update]
json = JSON.parse( File.read( options[:output] ) )
options[:from] = DateTime.parse( json.map { |day| day["date"] }.sort.last ).next_day
end
options[:to] ||= DateTime.now
exit if options[:from] > options[:to]
agent = login()
data = backup_all( agent, options[:nickname], options[:from], options[:to] )
if options[:output]
File.open( options[:output], "w" ) do |file|
file.puts( JSON.pretty_generate( options[:update] ? json.concat( data ) : data ) )
end
else
puts JSON.pretty_generate( data )
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment