rifmu / app.py
2ch's picture
Create app.py
8b591a5 verified
from asyncio import gather, sleep
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse, HTMLResponse
from httpx import AsyncClient, Timeout
from parsel import Selector
from py_fake_useragent import UserAgent
from pydantic import BaseModel
from uvicorn import run as uvicorn_run
ua = UserAgent(use_disk_cache=True)
async def rifme_net(word: str, client: AsyncClient) -> list:
url = f'https://rifme.net/r/{word}'
words = []
try:
response = await client.get(url, headers=ua.get_headers(url))
response.raise_for_status()
selector = Selector(text=response.text)
words = selector.css('ul#tochnye li.riLi::attr(data-w)').getall()
if not words:
words = [''.join(word.split()) for word in selector.css('ul#tochnye li.riLi').xpath('string()').getall()]
except Exception as exc:
print(exc)
return words
async def ryfma_com(word: str, client: AsyncClient) -> dict:
url = 'https://ryfma.com/graphql'
words = []
query = {
'operationName': 'searchRhyme',
'variables': {'word': word},
'query': '''
query searchRhyme($word: String!) {
searchRhyme(word: $word) {
sortedRhymes
}
}
'''
}
headers = ua.get_headers(url)
headers = headers.update({
'Content-Type': 'application/json',
})
try:
response = await client.post(url, headers=headers, json=query)
response.raise_for_status()
words = response.json().get('data', {}).get('searchRhyme', {}).get('sortedRhymes', [])
except Exception as exc:
print(exc)
return words
async def triumph_ru(word: str, client: AsyncClient) -> list[str]:
url = 'https://www.triumph.ru/html/serv/rifma-online.html'
words = []
headers = ua.get_headers(url)
data = {
'word': word,
'dzen': 'search',
'part': 'любая',
'slog': 'неважно',
'who': '100',
'start': ''
}
try:
response = await client.post(url, headers=headers, data=data)
response.raise_for_status()
selector = Selector(text=response.text)
words = [''.join(word.split()) for word in selector.css('#content table td').xpath('string()').getall()]
except Exception as exc:
print(exc)
return words
async def get_rhyme_for_word(word: str, exact_rhyme: bool, client: AsyncClient) -> list[str]:
async def fetch_results():
return await gather(rifme_net(word, client), ryfma_com(word, client), triumph_ru(word, client))
max_attempts = 4
delay = 2
for attempt in range(max_attempts):
results = await fetch_results()
seen = set()
final_results = []
if results:
for result in results:
if result and isinstance(result, (list, tuple)):
for _word in result:
_word = _word.strip()
if _word and len(_word) > 1:
if _word not in seen:
seen.add(_word)
if exact_rhyme:
if len(word) >= 2 and len(_word) >= 2 and word[-2:] == _word[-2:]:
final_results.append(_word)
else:
final_results.append(_word)
if final_results:
return final_results
if attempt < max_attempts - 1:
await sleep(delay)
return []
async def get_rhymes(words: list[str], exact_rhyme: bool) -> dict[str, list[str]]:
async with AsyncClient(follow_redirects=True, verify=False, timeout=Timeout(connect=15, read=30, write=15, pool=15), http2=True) as client:
tasks = [get_rhyme_for_word(word, exact_rhyme, client) for word in words]
results = await gather(*tasks)
return {word: result for word, result in zip(words, results)}
app = FastAPI()
class WordsRequest(BaseModel):
words: list[str]
exact_rhyme: bool = False
html_content = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>подбор рифм</title>
<style>
:root {
--bg-color: #121212;
--surface-color: #1e1e1e;
--input-bg: #2c2c2c;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-color: #7c4dff;
--accent-hover: #651fff;
--border-color: #333;
--success-color: #00e676;
--chip-bg: #333;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 900px;
padding: 2rem;
}
h1 {
text-align: center;
color: var(--accent-color);
font-weight: 300;
margin-bottom: 2rem;
letter-spacing: 1px;
}
.controls-area {
background-color: var(--surface-color);
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
margin-bottom: 2rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-secondary);
font-size: 0.9rem;
}
textarea {
width: 100%;
height: 100px;
background-color: var(--input-bg);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 8px;
padding: 10px;
font-size: 1rem;
resize: vertical;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s;
margin-bottom: 1rem;
}
textarea:focus {
border-color: var(--accent-color);
}
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
vertical-align: middle;
margin-right: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: var(--input-bg);
transition: .4s;
border-radius: 34px;
border: 1px solid var(--border-color);
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent-color);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.switch-label {
color: var(--text-primary);
cursor: pointer;
user-select: none;
}
button.search-btn {
padding: 12px 30px;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s, transform 0.1s;
}
button.search-btn:hover {
background-color: var(--accent-hover);
}
button.search-btn:active {
transform: scale(0.98);
}
button:disabled {
background-color: var(--input-bg);
color: var(--text-secondary);
cursor: not-allowed;
}
.loader {
display: none;
text-align: center;
margin: 1rem 0;
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.1);
width: 24px;
height: 24px;
border-radius: 50%;
border-left-color: var(--accent-color);
animation: spin 1s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.results-area {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.word-card {
background-color: var(--surface-color);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
border: 1px solid var(--border-color);
}
.word-title {
font-size: 1.2rem;
color: var(--success-color);
margin-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
text-transform: capitalize;
display: flex;
justify-content: space-between;
}
.count-badge {
font-size: 0.8rem;
color: var(--text-secondary);
background: var(--input-bg);
padding: 2px 8px;
border-radius: 10px;
}
.rhymes-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.rhyme-chip {
background-color: var(--chip-bg);
color: var(--text-primary);
padding: 8px 14px;
border-radius: 20px;
font-size: 0.95rem;
border: 1px solid var(--border-color);
transition: all 0.2s;
cursor: pointer;
user-select: none;
}
.rhyme-chip:hover {
background-color: var(--accent-color);
border-color: var(--accent-color);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.rhyme-chip:active {
transform: translateY(0);
}
.no-rhymes {
color: var(--text-secondary);
font-style: italic;
}
#toast {
visibility: hidden;
min-width: 250px;
margin-left: -125px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 25px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
border: 1px solid var(--accent-color);
opacity: 0;
transition: opacity 0.3s, bottom 0.3s;
}
#toast.show {
visibility: visible;
opacity: 1;
bottom: 50px;
}
</style>
</head>
<body>
<div class="container">
<h1>подбор рифмы</h1>
<div class="controls-area">
<label for="words">введи слова (через запятую или с новой строки):</label>
<textarea id="words" placeholder="пример: солнце, радость, кот"></textarea>
<div class="options-row">
<div style="display: flex; align-items: center;">
<label class="switch">
<input type="checkbox" id="exactRhyme">
<span class="slider"></span>
</label>
<span class="switch-label" onclick="document.getElementById('exactRhyme').click()">
более точная рифма (фильтр по окончанию)
</span>
</div>
<button id="searchBtn" class="search-btn" onclick="getRhymes()">найти рифмы</button>
</div>
</div>
<div class="loader" id="loader">
<span class="spinner"></span> ищем совпадения...
</div>
<div id="results" class="results-area"></div>
</div>
<div id="toast">слово скопировано</div>
<script>
async function getRhymes() {
const input = document.getElementById('words').value;
const exactRhyme = document.getElementById('exactRhyme').checked;
const resultsDiv = document.getElementById('results');
const loader = document.getElementById('loader');
const btn = document.getElementById('searchBtn');
const words = input.split(/[\\n,]+/).map(s => s.trim()).filter(s => s.length > 0);
if (words.length === 0) {
alert("нужно ввести хотя бы слово");
return;
}
resultsDiv.innerHTML = '';
loader.style.display = 'block';
btn.disabled = true;
try {
const response = await fetch('/get_rhymes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
words: words,
exact_rhyme: exactRhyme
})
});
if (!response.ok) throw new Error('ошибка сервера: ' + response.statusText);
const data = await response.json();
displayResults(data);
} catch (error) {
resultsDiv.innerHTML = `<div style="color: #ff5252; text-align: center;">ошибка: ${error.message}</div>`;
} finally {
loader.style.display = 'none';
btn.disabled = false;
}
}
function displayResults(data) {
const resultsDiv = document.getElementById('results');
for (const [word, rhymes] of Object.entries(data)) {
const card = document.createElement('div');
card.className = 'word-card';
const title = document.createElement('div');
title.className = 'word-title';
const titleText = document.createElement('span');
titleText.textContent = word;
const badge = document.createElement('span');
badge.className = 'count-badge';
badge.textContent = rhymes ? `${rhymes.length} шт.` : '0 шт.';
title.appendChild(titleText);
title.appendChild(badge);
const list = document.createElement('div');
list.className = 'rhymes-list';
if (rhymes && rhymes.length > 0) {
rhymes.forEach(rhyme => {
const chip = document.createElement('span');
chip.className = 'rhyme-chip';
chip.textContent = rhyme;
chip.title = "нажми, чтобы скопировать";
chip.onclick = () => {
copyToClipboard(rhyme);
};
list.appendChild(chip);
});
} else {
const noRhyme = document.createElement('span');
noRhyme.className = 'no-rhymes';
noRhyme.textContent = 'рифмы не найдены :(';
list.appendChild(noRhyme);
}
card.appendChild(title);
card.appendChild(list);
resultsDiv.appendChild(card);
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast(`скопировано: ${text}`);
}, (err) => {
console.error('ошибка копирования: ', err);
});
}
function showToast(message) {
const toast = document.getElementById("toast");
toast.textContent = message;
toast.className = "show";
setTimeout(() => { toast.className = toast.className.replace("show", ""); }, 2500);
}
</script>
</body>
</html>
"""
@app.get('/', response_class=HTMLResponse)
async def root():
return HTMLResponse(content=html_content)
@app.post('/get_rhymes')
async def get_rhymes_endpoint(request: WordsRequest):
if not request.words:
raise HTTPException(status_code=400, detail='список слов не может быть пустым')
if not isinstance(request.words, list):
raise HTTPException(status_code=400, detail='запрос должен быть списком слов')
clean_words = [w.strip() for w in request.words if w.strip()]
rhymes = await get_rhymes(clean_words, request.exact_rhyme)
return JSONResponse(content=rhymes)
if __name__ == '__main__':
uvicorn_run(app, host='0.0.0.0', port=7860)