Skip to content

Instantly share code, notes, and snippets.

@mattdsteele
Last active May 1, 2024 22:54
Show Gist options
  • Star 110 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save mattdsteele/7386ec363badfdeaad05a418b9a1f30a to your computer and use it in GitHub Desktop.
Save mattdsteele/7386ec363badfdeaad05a418b9a1f30a to your computer and use it in GitHub Desktop.

Paprika doesn't have their API documented, so this is me reverse-engineering it from an Android device

Live Demo

https://knowing-grain.glitch.me/

Code: https://glitch.com/edit/#!/knowing-grain

Sync

HTTP BASIC auth (emoji shrug), and send a GET to:

Syncing a particular recipe:

API uses BASIC auth; whatever your cloud sync is.

Services do not have CORS headers, so you can't invoke them directly from a browser :(

Saving/Updating a recipe

Send a POST to: https://www.paprikaapp.com/api/v1/sync/recipe/{uid-of-recipe}/

With your recipe in a multipart/form-data, in the data param. Example:

{
  "uid": "ccb42915-5fe9-425d-98da-c1ffbe420159",
  "name": "Test",
  "directions": "Do",
  "servings": "",
  "rating": 0,
  "difficulty": "Easy",
  "ingredients": "Ddddjdjdd\nDjdjdjdd\nDjdhdhdhdndnee ",
  "notes": "",
  "created": "2018-03-26 09:00:02",
  "image_url": null,
  "on_favorites": 0,
  "cook_time": "",
  "prep_time": "",
  "source": "",
  "source_url": "",
  "photo_hash": null,
  "photo": null,
  "nutritional_info": "",
  "scale": null,
  "deleted": false,
  "categories": [
    "cbaca738-cdfb-4150-960d-e1b1ac4cdcc3"
  ],
  "hash": "162e5ad0134e9398b98057aea951304780d0396582238320c28b34a7c35f841e"
}

The data param should be gzip-encoded

"Save Recipe" function

Still figuring this out 🤷‍♂️

Appears to be a POST to https://www.paprikaapp.com/api/v1/sync/recipes/, no auth

Sends a multipart/form-data with three fields:

Returns a text-plain (actually JSON) with a structure like:

{
    "result": {
        "cook_time": "4 mins. to 8 mins.",
        "difficulty": "",
        "directions": "To make the mix: Grind the oats in a food processor until they're chopped fine, but not a
powder.\n\nPut the flour, oats, and all other dry ingredients into a mixer with a paddle. Mix on slow speed, and
drizzle the vegetable oil into the bowl slowly while the mixer is running.\n\nStore in an airtight container for up
to two weeks at room temperature, or indefinitely in the refrigerator or freezer.\n\nTo make pancakes: Whisk
together 1 cup of mix, 1 cup of buttermilk (or a combination of half plain yogurt and half milk; or 3/4 cup liquid
whey), and 1 large egg. Don't worry if it seems thin at first: the oats will soak up the milk, and the mix will
thicken a bit as it stands.\n\nLet the batter stand for at least 20 minutes before cooking.\n\nHeat a lightly
greased griddle to 350°F (if you've got a griddle with a temperature setting; if not, medium-hot will do).\n\nDrop
the batter onto it in 1/4-cupfuls (a jumbo cookie scoop works well here) to make a 4\" diameter pancake. If you
have English muffin rings, use them; they make a perfectly round, evenly thick pancake.\n\nWhen the edges look dry
and bubbles come to the surface without breaking (after about 2 minutes, if your griddle is the correct
temperature), turn the pancake over to finish cooking on the second side, which will take about 2 minutes.\n\nServe
pancakes immediately, or stack and hold in a warm oven.\n\nYield: a batch using 1 cup of the mix will make about 5
to 8 pancakes, depending on size.",
        "image_url": "",
        "ingredients": "MIX\n4 cups King Arthur White Whole Wheat Flour or Organic White Whole Wheat Flour\n1 cup
King Arthur Unbleached All-Purpose Flour or Organic All-Purpose Flour\n3 1/2 cups old-fashioned or rolled oats\n3
tablespoons sugar\n3 tablespoons baking powder\n1 tablespoon salt\n1 tablespoon baking soda\n1 cup vegetable
oil\nPANCAKES\n1 cup homemade mix\n1 cup buttermilk, nut milk, or a combination of plain yogurt and milk; or 3/4
cup liquid whey\n1 large egg",
        "name": "Homemade Whole-Grain Pancake Mix",
        "notes": "",
        "nutritional_info": "Calories: 110\nTotal Carbohydrates: 12g\nCholesterol: 30mg\nTotal Fat: 5g\nDietary
fiber: 3g\nProtein: 4g\nSaturated fat: 1g\nAmount Per: 1 pancake (56g)\nSodium: 260mg\nSugar: 3g\nTrans Fat: 0g",
        "prep_time": "20 mins.",
        "servings": "10 cups dry mix",
        "total_time": ""
    }
}

You can then convert it to a recipe by making a UID and POSTing it to the URLs above

@kgmorales
Copy link

is it possible to save a new recipe using the v1 sync/recipe api?

I'm having a little trouble following how to save a new recipe through the POST.

@mattdsteele, thank you,

@mattdsteele
Copy link
Author

@kgmorales It's been a while since I've tried saving data, but yes, you should be able to save a new recipe. You'll have to generate a new UUID yourself. See https://gist.github.com/mattdsteele/7386ec363badfdeaad05a418b9a1f30a?permalink_comment_id=3916047#gistcomment-3916047 for a link to some example Python code.

@salvaom
Copy link

salvaom commented May 14, 2023

I'm still having trouble uploading a picture to a recipe, If I post to /api/v2/sync/photo/{a new uud} I get:
{'error': {'code': 0, 'message': 'Photo not found during photo upload.'}}

And if I post to /api/v2/sync/recipe/{recipe.uid} with the photo_upload argument I get
{'error': {'code': 0, 'message': 'Invalid photo filename.'}}

How can photos be uploaded to a recipe?

@jm-lamotte
Copy link

jm-lamotte commented Sep 18, 2023

I'm still having trouble uploading a picture to a recipe, If I post to /api/v2/sync/photo/{a new uud} I get: {'error': {'code': 0, 'message': 'Photo not found during photo upload.'}}

And if I post to /api/v2/sync/recipe/{recipe.uid} with the photo_upload argument I get {'error': {'code': 0, 'message': 'Invalid photo filename.'}}

How can photos be uploaded to a recipe?

Hello,
Here's the code I use for the upload (my photos are local, so I can't use requests to get the image content)

@dataclass
class RecipePhoto():
	uid: str = field(default_factory=lambda: str(uuid.uuid4()).upper())
	filename: str = ""
	name: str = ""
	order_flag: int = 1
	recipe_uid: str = ""
	hash: str = field(
		default_factory=lambda: hashlib.sha256(
			str(uuid.uuid4()).encode("utf-8")
		).hexdigest()
	)
	photo_url: Optional[str] = None
	deleted: bool = False

	def _request(self, method, path, token_r, authenticated=True, **kwargs):
		if authenticated:
			kwargs.setdefault("headers", {})[
				"Authorization"
			] = f"Bearer {token_r}"
		result = requests.request(method, path, **kwargs)
		result.raise_for_status()

		if "error" in result.json():
			raise RequestError()

		return result

	def as_gzip(self) -> bytes:
		return gzip.compress(self.as_json().encode("utf-8"))

	def as_json(self):
		return json.dumps(self.as_dict())

	def as_dict(self):
		return asdict(self)

	def calculate_hash(self) -> str:
		fields = self.as_dict()
		fields.pop("hash", None)

		return hashlib.sha256(
			json.dumps(fields, sort_keys=True).encode("utf-8")
		).hexdigest()

	def update_hash(self):
		self.hash = self.calculate_hash()

	def upload_photo(self, token):
		self.update_hash()
		files = {'data': self.as_gzip()}
		photo_data = self.get_photo_data()
		if self.photo_url :
			files['photo_upload'] = (self.filename, open(self.photo_url, 'rb'))
		self._request(
			"post",
			SYNC_PHOTO_URL(self.uid),
			token,
			files=files,
		)
		return self.uid

Then the call to the upload. RecipePhoto is a class with API fields for photos.

		Photo = RecipePhoto(
			filename = <the name of your photo file>,
			order_flag = 0, (Not sure how this is used)
			name = <the name of your photo>,
			recipe_uid = <the UID of the RECIPE you want to link the photo to>,
			photo_url = <LOCAL PATH TO THE PHOTO>,
			)

		Photo_UId = Photo.upload_photo(<your auth token>)

With this, I add the photo right after I created my recipe, and it shows when I display the photos of the recipe. It does not show in the recipe list, as the thumbnail is not set. I'm struggling on that part.

EDIT:
For the thumbnail, once you have uploaded your recipe and your photo, update the Recipe with photo_large (I use a description of the photo) and photo_url to the local url to your image

	Recipe.photo_large = <description>
	Recipe.photo_url = <path to the file>

Then update the recipe and attach the photo with a hah:

	def upload_recipe(self, token):

		files: Dict = {}

		if self.photo_url :
			self.photo = generate_uuid() + "." + self.photo_url.split(".")[-1]
			self.photo_hash = self.calculate_hash()
			files['photo_upload'] = (self.photo_url.split("/")[-1], open(self.photo_url, 'rb'))

		print(self)

		self.update_hash()

		files['data'] = self.as_gzip()


		self._request(
			"post",
			SYNC_RECIPE_URL(self.uid),
			token,
			files=files,
		)

		return self.uid


	Recipe_Definition["Recipe_Paprika_Uid"] = Recipe.upload_recipe(PAPRIKA_TOKEN)

@vostersc
Copy link

vostersc commented Feb 15, 2024

For anyone struggling w login, here is an example curl.

curl -X POST https://paprikaapp.com/api/v2/account/login -d 'email=ENTER_EMAIL&password=ENTER_PASSWORD

You'll get an auth token in response. You can use that for future requests. Not sure on TTL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment