{
  "openapi": "3.1.0",
  "info": {
    "title": "instadroid",
    "summary": "Atom feeds of an Instagram Following feed scraped from a real Android app.",
    "description": "When the server runs with `FEED_TOKEN`, every path except `/health` needs that token: as `Authorization: Bearer <token>`, as the password of HTTP basic auth, or as `?token=`. `/media/<file>` URLs written into the feeds carry a per-file `sig` instead.",
    "version": "0.1.0"
  },
  "paths": {
    "/instagram.xml": {
      "get": {
        "summary": "Posts feed",
        "description": "Stored posts as Atom, newest first. `user` narrows it to one account; `limit` is capped at\n500.",
        "operationId": "feed_instagram_xml_get",
        "parameters": [
          {
            "name": "user",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "User"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 200,
              "title": "Limit"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An Atom feed.",
            "content": {
              "application/atom+xml": {}
            }
          },
          "304": {
            "description": "Not Modified"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/stories.xml": {
      "get": {
        "summary": "Stories feed",
        "description": "Stored story frames as Atom, newest first; `limit` is capped at 500.",
        "operationId": "stories_feed_stories_xml_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 200,
              "title": "Limit"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An Atom feed.",
            "content": {
              "application/atom+xml": {}
            }
          },
          "304": {
            "description": "Not Modified"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/users": {
      "get": {
        "summary": "Accounts with stored posts",
        "description": "Every username with at least one stored post, sorted.",
        "operationId": "users_users_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array",
                  "title": "Response Users Users Get"
                }
              }
            }
          }
        }
      }
    },
    "/opml": {
      "get": {
        "summary": "Subscription list",
        "description": "One OPML outline nesting the aggregate feed, the stories feed, and one per-account feed\nper username in /users — a single FreshRSS import subscribes to everything this instance\nserves instead of pasting ?user= URLs in one at a time. `aggregate=0` leaves the aggregate feed\nout: a reader dedupes entries within a feed, so beside the per-account feeds it shows every post twice.",
        "operationId": "opml_opml_get",
        "parameters": [
          {
            "name": "aggregate",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true,
              "title": "Aggregate"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An OPML outline.",
            "content": {
              "text/x-opml": {}
            }
          },
          "304": {
            "description": "Not Modified"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Scraper health",
        "description": "`ok` unless no run has finished for too long or none has succeeded for too long (then 503,\nwith a `reason`). Never needs the feed token.",
        "operationId": "health_health_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Scraping looks stuck or keeps failing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "summary": "Status page",
        "description": "A plain-HTML page of recent runs, the device, and per-account totals.",
        "operationId": "status_page_status_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/control": {
      "get": {
        "summary": "Manual control state",
        "description": "Whether the manual lock holds scheduled runs, and whether a scrape-now request is pending.",
        "operationId": "control_state_control_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlState"
                }
              }
            }
          }
        }
      }
    },
    "/control/lock": {
      "post": {
        "summary": "Hold scheduled runs",
        "description": "Stop the scraper starting runs while the device is driven by hand. A run already going finishes.\nThe lock is ignored once it's LOCK_MAX_HOURS old; a hold the scraper raised itself never is.",
        "operationId": "lock_control_lock_post",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlState"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Release the lock",
        "description": "Release the manual lock and any hold: the scraper resumes on its schedule.",
        "operationId": "unlock_control_lock_delete",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlState"
                }
              }
            }
          }
        }
      }
    },
    "/control/scrape-now": {
      "post": {
        "summary": "Ask for a run now",
        "description": "Ask the poll loop to start a run within about 30 seconds instead of waiting out its sleep.\nRefused while locked, and within RUN_NOW_MIN_MINUTES of the last run finishing.",
        "operationId": "scrape_now_control_scrape_now_post",
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ControlState"
                }
              }
            }
          },
          "409": {
            "description": "The manual lock is in place."
          },
          "429": {
            "description": "Too soon."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ControlState": {
        "properties": {
          "locked": {
            "type": "boolean",
            "title": "Locked"
          },
          "scrape_now": {
            "type": "boolean",
            "title": "Scrape Now"
          },
          "hold": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Hold"
          }
        },
        "type": "object",
        "required": [
          "locked",
          "scrape_now"
        ],
        "title": "ControlState"
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "Health": {
        "properties": {
          "ok": {
            "type": "boolean",
            "title": "Ok"
          },
          "posts": {
            "type": "integer",
            "title": "Posts"
          },
          "reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason"
          }
        },
        "type": "object",
        "required": [
          "ok",
          "posts"
        ],
        "title": "Health"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      }
    }
  }
}
