Added Pixabay image search/import

This commit is contained in:
Mawoka
2023-06-23 00:07:33 +02:00
parent 7a13342ac1
commit c744ec162f
6 changed files with 306 additions and 1 deletions
+2
View File
@@ -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(
+1
View File
@@ -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"
+136
View File
@@ -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
+62
View File
@@ -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)
+16 -1
View File
@@ -24,6 +24,7 @@
import { getLocalization } from '$lib/i18n';
import { onMount } from 'svelte';
import Library from '$lib/editor/uploader/Library.svelte';
import Pixabay from '$lib/editor/uploader/Pixabay.svelte';
const { t } = getLocalization();
@@ -46,7 +47,9 @@
// eslint-disable-next-line no-unused-vars
Video,
// eslint-disable-next-line no-unused-vars
Library
Library,
// eslint-disable-next-line no-unused-vars
Pixabay
}
const uppy = new Uppy()
@@ -165,6 +168,14 @@
</BrownButton>
</div>
{/if}
<div class="w-full">
<BrownButton
on:click={() => {
selected_type = AvailableUploadTypes.Pixabay;
}}
>Pixabay
</BrownButton>
</div>
</div>
</div>
{:else if selected_type === AvailableUploadTypes.Image}
@@ -193,6 +204,10 @@
<div>
<Library bind:data {selected_question} bind:modalOpen />
</div>
{:else if selected_type === AvailableUploadTypes.Pixabay}
<div>
<Pixabay bind:data {selected_question} bind:modalOpen />
</div>
{/if}
</div>
{/if}
@@ -0,0 +1,89 @@
<!--
- 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/.
-->
<script lang="ts">
import type { EditorData } from '$lib/quiz_types';
import Spinner from '$lib/Spinner.svelte';
import BrownButton from '$lib/components/buttons/brown.svelte';
import { getLocalization } from '$lib/i18n';
export let data: EditorData;
export let selected_question: number;
export let modalOpen: boolean;
let page = 1;
let search_term = '';
let loading = false;
const { t } = getLocalization();
const set_image = async (id: string) => {
loading = true;
const res = await fetch(`/api/v1/pixabay/save?id=${id}`, {
method: 'POST'
});
const json = await res.json();
const storage_id = json.id;
if (selected_question === undefined) {
data.cover_image = storage_id;
} else if (selected_question === -1) {
data.background_image = storage_id;
} else {
data.questions[selected_question].image = storage_id;
}
modalOpen = false;
};
const fetch_data = async () => {
const res = await fetch(`/api/v1/pixabay/images?page=${page}&query=${search_term}`);
return await res.json();
};
let fetched_data = fetch_data();
</script>
{#await fetched_data}
<Spinner />
{:then data}
{#if loading}
<Spinner />
{:else}
<div class="flex w-screen p-8 h-full mt-8 mb-1">
<div
class="flex flex-col w-1/3 m-auto overflow-scroll h-full rounded p-4 gap-4 bg-white dark:bg-gray-700"
>
<div class="w-full flex gap-2">
<input
class="w-full outline-none p-1 rounded bg-gray-500"
bind:value={search_term}
/>
<div class="w-fit">
<BrownButton on:click={() => (fetched_data = fetch_data())}
>Search</BrownButton
>
</div>
</div>
{#each data.hits as image}
<div class="rounded border-2 border-[#B07156] p-2 flex-col flex gap-2">
<div>
<img
src={image.webformatURL}
loading="lazy"
alt="unavailable"
class="object-contain w-full h-full rounded max-h-[80vh]"
/>
</div>
<BrownButton
on:click={() => {
set_image(image.id);
}}>{$t('words.select')}</BrownButton
>
</div>
{/each}
</div>
</div>
{/if}
{/await}