✨ Added Pixabay image search/import
This commit is contained in:
@@ -32,6 +32,7 @@ from classquiz.routers import (
|
||||
admin,
|
||||
box_controller,
|
||||
quiztivity,
|
||||
pixabay,
|
||||
)
|
||||
from classquiz.socket_server import sio
|
||||
from classquiz.helpers import meilisearch_init, telemetry_ping
|
||||
@@ -76,6 +77,7 @@ async def auth_middleware_wrapper(request: Request, call_next):
|
||||
return await rememberme_middleware(request, call_next)
|
||||
|
||||
|
||||
app.include_router(pixabay.router, tags=["pixabay"], prefix="/api/v1/pixabay", include_in_schema=True)
|
||||
app.include_router(quiztivity.router, tags=["quiztivity"], prefix="/api/v1/quiztivity", include_in_schema=True)
|
||||
|
||||
app.include_router(
|
||||
|
||||
@@ -51,6 +51,7 @@ class Settings(BaseSettings):
|
||||
custom_openid_provider: CustomOpenIDProvider | None = None
|
||||
telemetry_enabled: bool = True
|
||||
free_storage_limit: int = 1074000000
|
||||
pixabay_api_key: str | None = None
|
||||
|
||||
# storage_backend
|
||||
storage_backend: str | None = "local"
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
import enum
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ImageType(str, enum.Enum):
|
||||
all = "all"
|
||||
photo = "photo"
|
||||
illustration = "illustration"
|
||||
vector = "vector"
|
||||
|
||||
|
||||
class Orientation(str, enum.Enum):
|
||||
all = ("all",)
|
||||
horizontal = "horizontal"
|
||||
vertical = "vertical"
|
||||
|
||||
|
||||
class Category(str, enum.Enum):
|
||||
background = "background"
|
||||
fashion = "fashion"
|
||||
nature = "nature"
|
||||
science = "science"
|
||||
education = "education"
|
||||
feelings = "feelings"
|
||||
health = "health"
|
||||
people = "people"
|
||||
religion = "religion"
|
||||
places = "places"
|
||||
animals = "animals"
|
||||
industry = "industry"
|
||||
computer = "computer"
|
||||
food = "food"
|
||||
sports = "sports"
|
||||
transportation = "transportation"
|
||||
travel = "travel"
|
||||
buildings = "buildings"
|
||||
business = "business"
|
||||
music = "music"
|
||||
|
||||
|
||||
class Colors(str, enum.Enum):
|
||||
grayscale = "grayscale"
|
||||
transparent = "transparent"
|
||||
red = "red"
|
||||
orange = "orange"
|
||||
yellow = "yellow"
|
||||
green = "green"
|
||||
turquoise = "turquoise"
|
||||
blue = "blue"
|
||||
lilac = "lilac"
|
||||
pink = "pink"
|
||||
white = "white"
|
||||
gray = "gray"
|
||||
black = "black"
|
||||
brown = "brown"
|
||||
|
||||
|
||||
class Order(str, enum.Enum):
|
||||
popular = "popular"
|
||||
latest = "latest"
|
||||
|
||||
|
||||
class BoolInput(str, enum.Enum):
|
||||
true = "true"
|
||||
false = "false"
|
||||
|
||||
|
||||
class GetImagesParams(BaseModel):
|
||||
q: str = ""
|
||||
lang: str = "en"
|
||||
id: str = ""
|
||||
image_type: ImageType = ImageType.all
|
||||
orientation: Orientation = Orientation.all
|
||||
category: Category | str = ""
|
||||
min_width: int = 0
|
||||
min_height: int = 0
|
||||
colors: Colors | str = ""
|
||||
editors_choice: BoolInput = BoolInput.false
|
||||
safesearch: BoolInput = BoolInput.false
|
||||
order: Order = Order.popular
|
||||
page: int = 1
|
||||
pretty: BoolInput = BoolInput.false
|
||||
|
||||
|
||||
class Hit(BaseModel):
|
||||
id: int
|
||||
pageURL: str
|
||||
type: str
|
||||
tags: str
|
||||
previewURL: str
|
||||
previewWidth: int
|
||||
previewHeight: int
|
||||
webformatURL: str
|
||||
webformatWidth: int
|
||||
webformatHeight: int
|
||||
largeImageURL: str
|
||||
imageWidth: int
|
||||
imageHeight: int
|
||||
imageSize: int
|
||||
views: int
|
||||
downloads: int
|
||||
collections: int
|
||||
likes: int
|
||||
comments: int
|
||||
user_id: int
|
||||
user: str
|
||||
userImageURL: str
|
||||
|
||||
|
||||
class GetImagesResponse(BaseModel):
|
||||
total: int
|
||||
totalHits: int
|
||||
hits: list[Hit]
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
async def get_images(api_key: str, params: GetImagesParams) -> GetImagesResponse:
|
||||
async with ClientSession() as session, session.get(
|
||||
"https://pixabay.com/api/", params={"key": api_key, **params.dict()}
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
return GetImagesResponse.parse_obj(await resp.json())
|
||||
else:
|
||||
raise NotFoundError
|
||||
@@ -0,0 +1,62 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from uuid import uuid4
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.db.models import User, StorageItem, PublicStorageItem
|
||||
from classquiz.helpers.pixabay import get_images, GetImagesParams, BoolInput, GetImagesResponse, NotFoundError
|
||||
from classquiz.config import settings, storage, arq
|
||||
|
||||
settings = settings()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/images")
|
||||
async def search_pixabay_images(query: str, page: int = 1, user: User = Depends(get_current_user)) -> GetImagesResponse:
|
||||
if settings.pixabay_api_key is None:
|
||||
raise HTTPException(status_code=423, detail="Pixabay not set up")
|
||||
return await get_images(settings.pixabay_api_key, GetImagesParams(q=query, safesearch=BoolInput.true, page=page))
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
async def save_pixabay_image(id: str, user: User = Depends(get_current_user)) -> PublicStorageItem:
|
||||
if settings.pixabay_api_key is None:
|
||||
raise HTTPException(status_code=423, detail="Pixabay not set up")
|
||||
if user.storage_used > settings.free_storage_limit:
|
||||
raise HTTPException(status_code=409, detail="Storage limit reached")
|
||||
try:
|
||||
images = await get_images(settings.pixabay_api_key, GetImagesParams(id=id, safesearch=BoolInput.true))
|
||||
except NotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Pixabay file not found")
|
||||
image = images.hits[0]
|
||||
file_id = uuid4()
|
||||
file_data = b""
|
||||
async with ClientSession() as session, session.get(image.largeImageURL) as resp:
|
||||
async for i in resp.content.iter_chunked(1024):
|
||||
file_data += i
|
||||
content_type = resp.headers.get("Content-Type")
|
||||
|
||||
if content_type is None:
|
||||
content_type = "image/*"
|
||||
file = BytesIO(file_data)
|
||||
await storage.upload(file_name=file_id.hex, file_data=file, mime_type=content_type)
|
||||
file_obj: StorageItem = StorageItem(
|
||||
id=file_id,
|
||||
uploaded_at=datetime.now(),
|
||||
mime_type=content_type,
|
||||
hash=None,
|
||||
user=user,
|
||||
size=0,
|
||||
deleted_at=None,
|
||||
alt_text=None,
|
||||
imported=True,
|
||||
)
|
||||
await file_obj.save()
|
||||
await arq.enqueue_job("calculate_hash", file_id.hex)
|
||||
return PublicStorageItem.from_db_model(file_obj)
|
||||
Reference in New Issue
Block a user