From aadd3eae93bb4aaa62abce62a4ee34b3967dd1fd Mon Sep 17 00:00:00 2001 From: sametcn99 Date: Mon, 2 Feb 2026 03:08:44 +0300 Subject: [PATCH] feat: add OpenAPI docs and Scalar API Reference under /docs #19 --- api/index.py | 31 ++- api/openapi.json | 569 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 api/openapi.json diff --git a/api/index.py b/api/index.py index ba1ccae..600b153 100644 --- a/api/index.py +++ b/api/index.py @@ -1,4 +1,4 @@ -from flask import Flask +from flask import Flask, send_from_directory from flask_cors import CORS import logging import api.config @@ -25,5 +25,34 @@ def home(): return 'Hello, World!' +@app.route('/openapi.json') +def openapi(): + return send_from_directory(app.root_path, 'openapi.json') + +@app.route('/docs') +def api_docs(): + return ''' + + + + Letterboxd API Reference + + + + + + + + + + ''' + + if __name__ == '__main__': app.run(debug=(api.config.ENV == 'dev'), port=api.config.PORT) diff --git a/api/openapi.json b/api/openapi.json new file mode 100644 index 0000000..879a5de --- /dev/null +++ b/api/openapi.json @@ -0,0 +1,569 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Letterboxd API", + "description": "A Flask-based API for exposing Letterboxd user review data via HTTP endpoints.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://letterboxd-api-zeta.vercel.app", + "description": "Production Server" + }, + { + "url": "http://localhost:5000", + "description": "Local Development Server" + } + ], + "paths": { + "/": { + "get": { + "summary": "Root Endpoint", + "description": "Returns a simple greeting.", + "responses": { + "200": { + "description": "Successful greeting", + "content": { + "text/plain": { + "schema": { + "type": "string", + "example": "Hello, World!" + } + } + } + } + } + } + }, + "/films": { + "get": { + "summary": "List Films", + "description": "Returns a paginated list of scraped film entries according to the query parameters.", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Page number", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of films per page", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "sort_by", + "in": "query", + "description": "Field to sort by", + "schema": { + "type": "string", + "default": "film_title", + "enum": [ + "film_id", + "film_title", + "film_link", + "avg_rating", + "like_ratio", + "num_likes", + "num_ratings", + "num_watches", + "metadata.avg_rating", + "metadata.year", + "metadata.runtime" + ] + } + }, + { + "name": "sort_order", + "in": "query", + "description": "Sort direction", + "schema": { + "type": "string", + "default": "asc", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "name": "avg_rating_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "avg_rating_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "like_ratio_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "like_ratio_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "num_likes_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "num_likes_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "num_ratings_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "num_ratings_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "num_watches_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "num_watches_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.avg_rating_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.avg_rating_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.year_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.year_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.runtime_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.runtime_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "film_title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "directors", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "actors", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "studios", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "themes", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "description", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "crew", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "genres", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "watched_by", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "not_watched_by", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "rated_by", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "not_rated_by", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of films", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + }, + "/films/{film_id}": { + "get": { + "summary": "Get Film by ID", + "description": "Returns the film entry for the specified film_id.", + "parameters": [ + { + "name": "film_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Film details", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/users": { + "get": { + "summary": "List Users", + "description": "Returns all users and their scraped review data.", + "responses": { + "200": { + "description": "List of users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + }, + "/users/{username}": { + "get": { + "summary": "Get User by Username", + "description": "Returns the user with the specified username and their scraped review data.", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "include_films", + "in": "query", + "description": "Whether to include reviews and watches arrays", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "User details", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/superlatives": { + "get": { + "summary": "List Superlatives", + "description": "Returns a list of superlatives grouped within categories.", + "responses": { + "200": { + "description": "List of superlatives", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/recommendations": { + "get": { + "summary": "Get Recommendations", + "description": "Return a list of recommendations for a group of users to watch.", + "parameters": [ + { + "name": "watchers", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "Comma-separated list of usernames" + }, + { + "name": "num_recs", + "in": "query", + "schema": { + "type": "integer", + "default": 3 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "ok_to_have_watched", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "max_ok_to_have_watched", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + }, + { + "name": "metadata.avg_rating_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.avg_rating_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.year_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.year_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.runtime_gte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "metadata.runtime_lte", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "directors", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "actors", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "studios", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "themes", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "description", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "crew", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "genres", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of recommendations", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file