Сохранил локальные изменения перед pull

This commit is contained in:
shaman_lesnoy 2025-01-12 07:15:58 +03:00
parent 10da9b3707
commit 5cadda8396
15 changed files with 815312 additions and 27 deletions

View file

@ -1,4 +1,22 @@
FROM ubuntu:latest
LABEL authors="arsen"
# Используем официальный образ Python как базовый
FROM python:3.11-slim
ENTRYPOINT ["top", "-b"]
# Устанавливаем системные зависимости для работы с анимацией и SQLite
RUN apt-get update && apt-get install -y \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем файлы приложения в контейнер
COPY . /app
# Устанавливаем необходимые Python библиотеки
RUN pip install --no-cache-dir -r requirements.txt
# Открываем порты для работы приложения
EXPOSE 5000
# Запускаем приложение
CMD ["hypercorn", "main:app", "--bind", "0.0.0.0:5000"]

Binary file not shown.

View file

@ -0,0 +1,21 @@
services:
web:
build: . # Путь к Dockerfile (в текущей директории)
container_name: workshop
# ports:
# - "80:5000" # Проброс портов
volumes:
- /storage/public/complete/workshop/:/data
networks:
- all
labels:
- traefik.enable=true
- traefik.http.routers.workshop.rule=Host(`csgoworkshop.ru`) || Host(`www.csgoworkshop.ru`)
- traefik.http.routers.workshop.entrypoints=websecure, web
- traefik.http.routers.workshop.tls=true
- traefik.http.routers.workshop.tls.certresolver=production-cloudflare
restart: unless-stopped
networks:
all:
external: true

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -172,3 +172,76 @@ body {
top: 210px;
left: 660px;
}
@media screen and (max-width: 768px) {
body {
flex-direction: column;
align-items: center;
padding: 0 10px;
}
.main-container {
padding-top: 20px;
}
.image-wrapper {
width: 100%;
flex-direction: column;
align-items: center;
padding: 10px;
}
.image-container {
width: 100%;
height: auto;
padding: 0;
}
.image-container img {
width: 100%;
height: auto;
}
.details-container {
width: 100%;
margin-top: 15px;
margin-left: 0;
padding: 15px;
}
.download-btn {
position: static;
margin-top: 15px;
width: 100%;
height: 40px;
font-size: 18px;
}
.game-mode,
.tags,
.file-size,
.added-time {
position: static;
margin-top: 10px;
width: 100%;
text-align: left;
}
.dynamic-data {
margin-left: 0;
font-size: 14px;
}
.youtube-container {
position: static;
margin-top: 15px;
width: 100%;
height: auto;
}
.youtube-container iframe {
width: 100%;
height: auto;
}
}

View file

@ -4,10 +4,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workshop</title>
<title>workshop</title>
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
<link rel="icon" href="https://csgoworkshop.ru/favicon.ico" type="image/x-icon">
</head>
<body>
<div class="main-container">
<div class="image-wrapper">
@ -21,7 +23,7 @@
{{ description }}
</div>
<a href="{{ url_for('download_bsp', image_path=image_url) }}">
<button class="download-btn">🡇 Скачать</button>
<button class="download-btn">Скачать</button>
</a>
</div>

View file

@ -6,18 +6,64 @@ body {
display: block;
}
/* кнопка "Связаться" */
.contact-button {
position: absolute;
top: 10px;
right: 50px;
font-size: 14px;
color: #417A9B;
text-decoration: none;
font-weight: bold;
background-color: transparent;
border: none;
cursor: pointer;
}
.contact-button:hover {
text-decoration: underline;
}
.top-bar {
background-color: #171D25;
width: 100%;
height: 105px;
display: flex;
justify-content: center;
justify-content: space-between; /* Элементы слева и справа */
align-items: center;
position: fixed;
top: 0;
z-index: 1000;
padding-left: 20px; /* Отступ слева */
padding-right: 20px; /* Отступ справа */
}
.left-text {
color: white;
font-family: Arial, sans-serif;
font-size: 14px;
margin-right: 15px; /* Отступ справа для текста */
}
.left-text p {
margin: 0;
padding: 0;
}
.center-text {
color: white;
font-family: Arial, sans-serif;
font-size: 28px;
text-align: center;
flex-grow: 1; /* Элемент займет всю доступную ширину */
margin-left: -500px; /* Это выравнивает текст по правому краю */
margin-right: auto; /* Это выравнивает текст по левому краю */
}
.main-container {
display: flex;
justify-content: center;
@ -485,3 +531,136 @@ body {
cursor: pointer;
accent-color: #63AFCD;
}
@media screen and (max-width: 768px) {
body {
font-size: 14px;
}
.top-bar {
height: 80px;
}
.main-container {
flex-direction: column;
align-items: center;
margin-top: 80px;
padding: 0 10px;
}
.cards-container {
grid-template-columns: 1fr;
grid-gap: 20px;
width: 100%;
justify-items: center;
}
.card {
width: 90%; /* Карточки занимают почти весь экран */
max-width: 300px; /* Ограничение по ширине */
}
.right-rectangle {
width: 100%;
height: auto;
margin-left: 0;
}
.game-modes {
position: static;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
}
@media screen and (max-width: 768px) {
.modal {
width: 90%;
height: auto;
padding: 15px;
max-width: 500px; /* Ограничиваем максимальную ширину для больших мобильных устройств */
}
.modal-content {
padding: 15px;
}
.close-btn {
top: -30px;
right: -10px;
font-size: 25px;
}
.modal-content h2 {
margin-left: 15px;
font-size: 18px;
}
.modal-rectangle {
width: 100%;
height: 45px;
padding: 0 15px;
}
.modal-rectangle .text-left,
.modal-rectangle .text-center {
font-size: 14px;
padding: 5px 0;
}
.date-input {
width: 100%;
height: 35px;
font-size: 16px;
margin-top: 10px;
}
.cancel-button,
.ok-button {
position: static;
margin-top: 20px;
width: 100%;
height: 40px;
font-size: 16px;
}
.cancel-button {
margin-bottom: 10px;
}
.ok-button {
margin-bottom: 10px;
}
}
.pagination {
flex-wrap: wrap;
}
.filter-stars {
width: 90%;
position: static;
margin: 10px 0;
}
.search-container {
margin-top: 20px;
}
.search-input {
width: 90%;
}
.sort-button-container {
flex-wrap: wrap;
justify-content: center;
}
.sort-button {
width: auto;
margin: 5px;
}
}

View file

@ -4,14 +4,37 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workshop</title>
<title>workshop</title>
<link rel="icon" href="https://csgoworkshop.ru/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('static', filename='workshop.css') }}">
<script src="{{ url_for('static', filename='workshop.js') }}" defer></script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(99366428, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/99366428" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<div class="top-bar">
<h1>Добро пожаловать в Workshop</h1>
<div class="left-text">
<p>Сделал: ©️𝙎𝙃∆𝙈∆𝙉©️</p>
<p>Неофициальный сайт<br>с картами для CS:GO<br>из мастерской Steam</p>
</div>
<h1 class="center-text">CS:GO Workshop</h1>
<a href="https://cloud.s.shsr.ru/apps/forms/s/xr8NyqdpsodwcNnRBSrMzJ4N" class="contact-button" target="_blank">Связаться</a>
</div>
<div class="main-container">

128
main.py
View file

@ -5,12 +5,14 @@ import aiosqlite
from quart import Quart, render_template, request, send_from_directory, Response
from datetime import datetime
from babel.dates import format_datetime
import locale
locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8')
from urllib.parse import quote
app = Quart(__name__, template_folder='frontend', static_folder='frontend')
DB_PATH = 'maps.db'
DATA = "/data"
GAME_MODES = {
"Classic": "Классический",
@ -28,6 +30,7 @@ last_download_times = {}
DOWNLOAD_COOLDOWN = 10
async def get_maps(page=1, per_page=30):
print(f"Запрос карт на странице {page}, с {per_page} картами на странице.")
async with aiosqlite.connect(DB_PATH) as conn:
cursor = await conn.cursor()
@ -42,71 +45,90 @@ async def get_maps(page=1, per_page=30):
maps = await cursor.fetchall()
print(f"Получено {len(maps)} карт на странице {page}.")
return maps
def get_image_path(filepath):
image_path = os.path.join('J:/public/complete/workshop', filepath)
print(f"Получение пути изображения для файла: {filepath}")
image_path = os.path.join(DATA, filepath)
if not os.path.exists(image_path):
return "http://127.0.0.1:5000/images/image.jpg"
return f"http://127.0.0.1:5000/images/{filepath.split('/')[0]}/{filepath.split('/')[1]}/{filepath.split('/')[1]}.jpg"
print("Изображение не найдено, возвращаем дефолтное.")
return "/images/image.jpg"
return f"/images/{filepath.split('/')[0]}/{filepath.split('/')[1]}/{filepath.split('/')[1]}.jpg"
def get_star_image(stars):
print(f"Получение изображения для {stars} звезд.")
if stars is None or stars == 0:
return "http://127.0.0.1:5000/stars/0-star.png"
return f"http://127.0.0.1:5000/stars/{stars}-star.png"
return "/stars/0-star.png"
return f"/stars/{stars}-star.png"
@app.route('/images/<path:filename>')
async def serve_image(filename):
image_path = os.path.join('J:/public/complete/workshop', filename)
print(f"Запрос изображения с именем: {filename}")
image_path = os.path.join(DATA, filename)
if os.path.exists(image_path):
return await send_from_directory('J:/public/complete/workshop', filename)
print(f"Изображение {filename} найдено и отправляется.")
return await send_from_directory(DATA, filename)
else:
default_image_path = os.path.join('J:/public/complete/workshop', 'image.jpg')
default_image_path = os.path.join(DATA, 'image.jpg')
if os.path.exists(default_image_path):
return await send_from_directory('J:/public/complete/workshop', 'image.jpg')
print("Изображение не найдено, отправляем дефолтное.")
return await send_from_directory(DATA, 'image.jpg')
print("Не найдено ни одного изображения.")
return "Default image not found", 404
@app.route('/stars/<filename>')
async def serve_star_image(filename):
star_path = os.path.join('J:/public/complete/workshop/stars', filename)
print(f"Запрос изображения звезды: {filename}")
stars = os.path.join(DATA, 'stars')
star_path = os.path.join(stars, filename)
if os.path.exists(star_path):
return await send_from_directory('J:/public/complete/workshop/stars', filename)
print(f"Звезда {filename} найдена и отправляется.")
return await send_from_directory(stars, filename)
else:
print("Изображение звезды не найдено.")
return "Star image not found", 404
@app.route('/download_bsp')
async def download_bsp():
user_ip = request.remote_addr
print(f"Запрос на скачивание от IP: {user_ip}")
current_time = time.time()
last_time = last_download_times.get(user_ip, 0)
if current_time - last_time < DOWNLOAD_COOLDOWN:
wait_time = DOWNLOAD_COOLDOWN - (current_time - last_time)
print(f"Пользователь должен подождать {int(wait_time)} секунд до следующего скачивания.")
return f"Please wait {int(wait_time)} seconds before downloading again.", 429
last_download_times[user_ip] = current_time
print("Тайм-аут скачивания обновлен.")
image_path = request.args.get('image_path')
if not image_path:
print("Не указан путь к изображению.")
return "No image path provided", 400
image_folder = os.path.dirname(image_path.replace("http://127.0.0.1:5000/images/", ""))
image_folder = os.path.dirname(image_path.replace("/images/", ""))
bsp_filename = None
for file in os.listdir(os.path.join('J:/public/complete/workshop', image_folder)):
for file in os.listdir(os.path.join(DATA, image_folder)):
if file.endswith('.bsp'):
bsp_filename = file
break
if not bsp_filename:
print("Не найден .bsp файл в той же директории.")
return "No .bsp file found in the same directory", 404
file_path = os.path.join('J:/public/complete/workshop', image_folder, bsp_filename)
file_path = os.path.join(DATA, image_folder, bsp_filename)
print(f"Найден файл для скачивания: {file_path}")
SPEED_LIMIT = 40 * 1024 * 1024 // 8
SPEED_LIMIT = 60 * 1024 * 1024 // 8
async def file_stream():
print("Начало передачи файла по частям.")
with open(file_path, 'rb') as f:
while chunk := f.read(SPEED_LIMIT):
yield chunk
@ -119,6 +141,7 @@ async def download_bsp():
@app.route('/main')
async def main_page():
print("Запрос страницы карты с параметрами:", request.args)
image_url = request.args.get('image_url', 'default_image.jpg')
map_title = request.args.get('map_title', 'Default Map Title')
@ -159,12 +182,15 @@ async def main_page():
tags = 'Не найдено'
if file_path:
file_size = os.path.getsize(os.path.join('J:/public/complete/workshop', file_path))
file_size = os.path.getsize(os.path.join(DATA, file_path))
file_size_mb = file_size / (1024 * 1024)
file_size_display = f"{file_size_mb:.2f} MB"
else:
file_size_display = 'Не найден'
print(f"Отправка страницы карты {map_title} с данными:")
print(f"Игра: {game_mode}, Теги: {tags}, Размер файла: {file_size_display}, Время добавления: {added_time}")
return await render_template(
'main.html',
image_url=image_url,
@ -180,6 +206,7 @@ async def main_page():
@app.route('/')
async def index():
print("Запрос главной страницы с параметрами фильтрации:", request.args)
page = int(request.args.get('page', 1))
selected_game_modes = request.args.getlist('game_modes')
start_date = request.args.get('start_date')
@ -230,6 +257,7 @@ async def index():
([f'stars={selected_stars}'] if selected_stars else [])
)
print(f"Общее количество карт: {total_maps}, Страниц: {total_pages}")
return await render_template(
'workshop.html',
maps_data=maps_data,
@ -243,6 +271,7 @@ async def index():
)
async def get_maps_filtered(page=1, selected_game_modes=None, start_date=None, end_date=None, search_title=None, selected_stars=None):
print(f"Фильтрация карт с параметрами: {locals()}")
async with aiosqlite.connect(DB_PATH) as conn:
cursor = await conn.cursor()
@ -279,7 +308,68 @@ async def get_maps_filtered(page=1, selected_game_modes=None, start_date=None, e
await cursor.execute(query, params)
maps = await cursor.fetchall()
print(f"Найдено {len(maps)} карт после фильтрации.")
return maps
@app.route('/favicon.ico')
async def favicon():
return await send_from_directory(os.getcwd(), 'favicon.ico')
@app.route('/sitemap.xml')
async def sitemap():
domain = "https://csgoworkshop.ru"
async with aiosqlite.connect(DB_PATH) as conn:
cursor = await conn.cursor()
# Получаем список всех карт
await cursor.execute('SELECT Title FROM maps')
maps = await cursor.fetchall()
# Создаем базовые URL для sitemap
urls = [
f"{domain}/",
f"{domain}/main",
]
# Добавляем URL-адреса для всех карт
for map_title, in maps:
encoded_title = quote(map_title)
urls.append(f"{domain}/main?map_title={encoded_title}")
# Генерируем XML
sitemap_xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
sitemap_xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for url in urls:
sitemap_xml += " <url>\n"
sitemap_xml += f" <loc>{url}</loc>\n"
sitemap_xml += f" <lastmod>{datetime.utcnow().strftime('%Y-%m-%d')}</lastmod>\n"
sitemap_xml += " <changefreq>weekly</changefreq>\n"
sitemap_xml += " <priority>0.8</priority>\n"
sitemap_xml += " </url>\n"
sitemap_xml += '</urlset>'
return Response(sitemap_xml, content_type='application/xml')
@app.route('/robots.txt')
async def robots_txt():
content = """
User-agent: *
Disallow:
"""
return Response(content, content_type='text/plain')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)
print("Запуск приложения...")
import hypercorn.asyncio
from hypercorn.config import Config
config = Config()
config.bind = ["0.0.0.0:5000"]
hypercorn.asyncio.run(app, config)

Binary file not shown.

View file

View file

@ -0,0 +1,4 @@
aiosqlite~=0.20.0
Hypercorn~=0.17.3
Quart~=0.19.6
babel~=2.16.0

814875
sitemap.xml Normal file

File diff suppressed because it is too large Load diff