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