Update files

This commit is contained in:
glpshchn 2025-12-15 23:37:41 +03:00
parent 046066a079
commit 56538d098f
12 changed files with 385 additions and 53 deletions

126
DELETE_MUSIC.md Normal file
View File

@ -0,0 +1,126 @@
# Скрипт удаления всех альбомов и треков
## Описание
Скрипт `deleteAllMusic.js` безвозвратно удаляет все музыкальные данные из базы данных:
- Все треки (Tracks)
- Все альбомы (Albums)
- Все избранные треки (FavoriteTrack)
- Все ссылки на треки в постах (attachedTrack устанавливается в null)
**⚠️ ВНИМАНИЕ: Это действие необратимо!**
## Использование
### Для Windows (CMD/PowerShell):
```cmd
cd backend
node scripts\deleteAllMusic.js
```
### Для Linux/Mac:
```bash
cd backend
node scripts/deleteAllMusic.js
```
### Или из корня проекта:
```cmd
node backend/scripts/deleteAllMusic.js
```
## Что делает скрипт
1. **Подключается к MongoDB** используя настройки из `.env`
2. **Показывает статистику** текущего количества записей
3. **Удаляет данные в следующем порядке:**
- Избранные треки (FavoriteTrack)
- Обнуляет attachedTrack в постах
- Удаляет все треки (Track)
- Удаляет все альбомы (Album)
4. **Показывает финальную статистику**
## Пример вывода
```
🔌 Подключение к MongoDB...
✅ Подключено к MongoDB
📊 Текущая статистика:
- Треков: 150
- Альбомов: 25
- Избранных треков: 500
- Постов с прикрепленными треками: 80
🗑️ Начало удаления...
1⃣ Удаление избранных треков...
✅ Удалено избранных треков: 500
2⃣ Обнуление прикрепленных треков в постах...
✅ Обновлено постов: 80
3⃣ Удаление треков...
✅ Удалено треков: 150
4⃣ Удаление альбомов...
✅ Удалено альбомов: 25
📊 Финальная статистика:
- Треков: 0
- Альбомы: 0
- Избранных треков: 0
- Постов с прикрепленными треками: 0
Все альбомы и треки успешно удалены!
🔌 Отключено от MongoDB
```
## Важные замечания
- ⚠️ **Физические файлы не удаляются** - скрипт удаляет только записи из базы данных. Файлы в `backend/uploads/music/` остаются на диске. Для полной очистки нужно вручную удалить файлы.
- ⚠️ **Исполнители (Artists) не удаляются** - они остаются в базе, так как могут быть переиспользованы при загрузке новых треков.
- ✅ **Посты не удаляются** - только обнуляется поле `attachedTrack`.
## Дополнительная очистка файлов
Если нужно также удалить физические файлы (аудио и обложки):
### Windows (PowerShell):
```powershell
# Удалить все файлы в папке музыки (сохранив структуру папок)
Remove-Item -Path "backend\uploads\music\*" -Include *.mp3,*.wav,*.ogg,*.m4a,*.flac,*.jpg,*.jpeg,*.png -Force
# Или полностью удалить папку и создать заново
Remove-Item -Path "backend\uploads\music" -Recurse -Force
New-Item -Path "backend\uploads\music" -ItemType Directory
```
### Linux/Mac:
```bash
# Удалить все файлы в папке музыки
rm -f backend/uploads/music/*.{mp3,wav,ogg,m4a,flac,jpg,jpeg,png}
# Или полностью удалить папку и создать заново
rm -rf backend/uploads/music
mkdir -p backend/uploads/music
```
## Восстановление после удаления
После выполнения скрипта невозможно восстановить удаленные данные из базы данных. Если у вас есть резервная копия (backup), вы можете восстановить данные из неё.
Для создания резервной копии перед удалением используйте:
```cmd
node backend/scripts/backup.js
```

View File

@ -0,0 +1,86 @@
# Исправление ошибки 413 при загрузке альбомов
## Проблема
При загрузке ZIP альбомов возникает ошибка 413 "Request Entity Too Large", даже после увеличения лимитов в Express и multer.
## Решение
Ошибка 413 возникает на уровне Nginx, который проксирует запросы к backend. Нужно увеличить `client_max_body_size` в конфигурации Nginx.
### Вариант 1: Если используется Nginx для проксирования к backend
Добавьте или увеличьте `client_max_body_size` в конфигурации Nginx:
```nginx
server {
listen 80;
server_name your-domain.com;
# Увеличить лимит загрузки до 110MB (больше чем на backend для безопасности)
client_max_body_size 110M;
location /api {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Увеличить таймауты для больших загрузок
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
}
}
```
### Вариант 2: Если используется docker-compose с nginx
Обновите `nginx-moderation-production.conf` или создайте отдельный конфиг для основного приложения:
```nginx
# Увеличить лимит загрузки
client_max_body_size 110M;
# Увеличить таймауты
client_body_timeout 300s;
client_header_timeout 300s;
```
### Вариант 3: Для production на сервере
Если приложение развернуто на сервере, нужно обновить конфигурацию Nginx:
1. Отредактируйте конфигурацию:
```bash
sudo nano /etc/nginx/sites-available/nakama
```
2. Добавьте или обновите:
```nginx
client_max_body_size 110M;
```
3. Проверьте конфигурацию:
```bash
sudo nginx -t
```
4. Перезапустите Nginx:
```bash
sudo systemctl reload nginx
```
## Проверка
После применения изменений попробуйте загрузить ZIP альбом размером до 105MB. Ошибка 413 должна исчезнуть.
## Примечание
Лимиты должны быть установлены в следующем порядке (от большего к меньшему):
1. Nginx: `client_max_body_size 110M` (больше всех)
2. Express: `limit: '105mb'`
3. Multer: `fileSize: 105 * 1024 * 1024`
Это обеспечивает, что запрос доходит до backend без ошибок.

View File

@ -215,6 +215,12 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
await post.save(); await post.save();
await post.populate('author', 'username firstName lastName photoUrl'); await post.populate('author', 'username firstName lastName photoUrl');
if (post.attachedTrack) {
await post.populate({
path: 'attachedTrack',
populate: { path: 'artist album' }
});
}
// Увеличить счетчики использования тегов // Увеличить счетчики использования тегов
if (parsedTags.length > 0) { if (parsedTags.length > 0) {

View File

@ -0,0 +1,98 @@
/**
* Скрипт для удаления всех альбомов и треков из базы данных
*
* ВНИМАНИЕ: Этот скрипт безвозвратно удалит:
* - Все треки (Tracks)
* - Все альбомы (Albums)
* - Все избранные треки (FavoriteTrack)
* - Все ссылки на треки в постах (attachedTrack будет установлен в null)
*
* Использование:
* node backend/scripts/deleteAllMusic.js
*/
require('dotenv').config({ path: require('path').join(__dirname, '../.env') });
const mongoose = require('mongoose');
const Track = require('../models/Track');
const Album = require('../models/Album');
const FavoriteTrack = require('../models/FavoriteTrack');
const Post = require('../models/Post');
const config = require('../config/index');
async function deleteAllMusic() {
try {
console.log('🔌 Подключение к MongoDB...');
await mongoose.connect(config.mongoUri);
console.log('✅ Подключено к MongoDB\n');
// Подсчет перед удалением
const tracksCount = await Track.countDocuments();
const albumsCount = await Album.countDocuments();
const favoritesCount = await FavoriteTrack.countDocuments();
const postsWithTracksCount = await Post.countDocuments({ attachedTrack: { $ne: null } });
console.log('📊 Текущая статистика:');
console.log(` - Треков: ${tracksCount}`);
console.log(` - Альбомов: ${albumsCount}`);
console.log(` - Избранных треков: ${favoritesCount}`);
console.log(` - Постов с прикрепленными треками: ${postsWithTracksCount}\n`);
if (tracksCount === 0 && albumsCount === 0) {
console.log('✅ В базе данных нет треков и альбомов для удаления');
await mongoose.disconnect();
return;
}
console.log('🗑️ Начало удаления...\n');
// 1. Удалить все избранные треки
console.log('1⃣ Удаление избранных треков...');
const favoritesResult = await FavoriteTrack.deleteMany({});
console.log(` ✅ Удалено избранных треков: ${favoritesResult.deletedCount}`);
// 2. Обнулить attachedTrack во всех постах
console.log('\n2⃣ Обнуление прикрепленных треков в постах...');
const postsResult = await Post.updateMany(
{ attachedTrack: { $ne: null } },
{ $set: { attachedTrack: null } }
);
console.log(` ✅ Обновлено постов: ${postsResult.modifiedCount}`);
// 3. Удалить все треки
console.log('\n3⃣ Удаление треков...');
const tracksResult = await Track.deleteMany({});
console.log(` ✅ Удалено треков: ${tracksResult.deletedCount}`);
// 4. Удалить все альбомы
console.log('\n4⃣ Удаление альбомов...');
const albumsResult = await Album.deleteMany({});
console.log(` ✅ Удалено альбомов: ${albumsResult.deletedCount}`);
// Финальная статистика
console.log('\n📊 Финальная статистика:');
const finalTracksCount = await Track.countDocuments();
const finalAlbumsCount = await Album.countDocuments();
const finalFavoritesCount = await FavoriteTrack.countDocuments();
const finalPostsWithTracksCount = await Post.countDocuments({ attachedTrack: { $ne: null } });
console.log(` - Треков: ${finalTracksCount}`);
console.log(` - Альбомов: ${finalAlbumsCount}`);
console.log(` - Избранных треков: ${finalFavoritesCount}`);
console.log(` - Постов с прикрепленными треками: ${finalPostsWithTracksCount}`);
console.log('\n✅ Все альбомы и треки успешно удалены!');
} catch (error) {
console.error('❌ Ошибка при удалении:', error);
process.exit(1);
} finally {
await mongoose.disconnect();
console.log('\n🔌 Отключено от MongoDB');
process.exit(0);
}
}
// Запуск скрипта
deleteAllMusic();

View File

@ -1,4 +1,5 @@
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { useLocation } from 'react-router-dom'
import { X, Play, Pause, SkipBack, SkipForward, Heart, Download, Music, Volume2, VolumeX } from 'lucide-react' import { X, Play, Pause, SkipBack, SkipForward, Heart, Download, Music, Volume2, VolumeX } from 'lucide-react'
import { useMusicPlayer } from '../contexts/MusicPlayerContext' import { useMusicPlayer } from '../contexts/MusicPlayerContext'
import { hapticFeedback, getTelegramUser } from '../utils/telegram' import { hapticFeedback, getTelegramUser } from '../utils/telegram'
@ -7,6 +8,7 @@ import api from '../utils/api'
import './FullPlayer.css' import './FullPlayer.css'
export default function FullPlayer() { export default function FullPlayer() {
const location = useLocation()
const { const {
currentTrack, currentTrack,
isPlaying, isPlaying,
@ -23,11 +25,12 @@ export default function FullPlayer() {
changeVolume, changeVolume,
setIsExpanded setIsExpanded
} = useMusicPlayer() } = useMusicPlayer()
const [isFavorite, setIsFavorite] = useState(false) const [isFavorite, setIsFavorite] = useState(false)
const [showVolume, setShowVolume] = useState(false) const [showVolume, setShowVolume] = useState(false)
if (!isExpanded || !currentTrack) return null // Скрыть плеер на странице profile
if (location.pathname === '/profile' || !isExpanded || !currentTrack) return null
const formatTime = (seconds) => { const formatTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00' if (!seconds || isNaN(seconds)) return '0:00'

View File

@ -1,6 +1,6 @@
.ladder-button { .ladder-button {
position: fixed; position: fixed;
bottom: 100px; bottom: 140px;
right: 16px; right: 16px;
width: 56px; width: 56px;
height: 56px; height: 56px;

View File

@ -7,8 +7,8 @@ export default function LadderButton() {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
// Скрыть кнопку на странице ladder // Скрыть кнопку на странице ladder и profile
if (location.pathname === '/ladder') { if (location.pathname === '/ladder' || location.pathname === '/profile') {
return null return null
} }

View File

@ -1,9 +1,11 @@
import { useLocation } from 'react-router-dom'
import { Play, Pause, SkipForward, Music } from 'lucide-react' import { Play, Pause, SkipForward, Music } from 'lucide-react'
import { useMusicPlayer } from '../contexts/MusicPlayerContext' import { useMusicPlayer } from '../contexts/MusicPlayerContext'
import { hapticFeedback } from '../utils/telegram' import { hapticFeedback } from '../utils/telegram'
import './MiniPlayer.css' import './MiniPlayer.css'
export default function MiniPlayer() { export default function MiniPlayer() {
const location = useLocation()
const { const {
currentTrack, currentTrack,
isPlaying, isPlaying,
@ -14,7 +16,8 @@ export default function MiniPlayer() {
toggleExpanded toggleExpanded
} = useMusicPlayer() } = useMusicPlayer()
if (!currentTrack) return null // Скрыть плеер на странице profile
if (location.pathname === '/profile' || !currentTrack) return null
const progressPercent = duration > 0 ? (progress / duration) * 100 : 0 const progressPercent = duration > 0 ? (progress / duration) * 100 : 0

View File

@ -1,11 +1,12 @@
.music-attachment { .music-attachment {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 8px;
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
border-radius: 12px; border-radius: 8px;
padding: 12px; padding: 8px 12px;
margin-top: 8px;
transition: all 0.2s; transition: all 0.2s;
} }
@ -14,9 +15,9 @@
} }
.music-attachment-cover { .music-attachment-cover {
width: 48px; width: 32px;
height: 48px; height: 32px;
border-radius: 8px; border-radius: 6px;
background: var(--bg-primary); background: var(--bg-primary);
display: flex; display: flex;
align-items: center; align-items: center;
@ -36,12 +37,15 @@
} }
.music-attachment-info { .music-attachment-info {
display: flex;
align-items: center;
gap: 6px;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.music-attachment-title { .music-attachment-title {
font-size: 14px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
white-space: nowrap; white-space: nowrap;
@ -50,13 +54,24 @@
} }
.music-attachment-artist { .music-attachment-artist {
font-size: 12px; font-size: 11px;
color: var(--text-secondary); color: var(--text-secondary);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.music-attachment-title::after {
content: '';
color: var(--text-secondary);
margin: 0 4px;
font-weight: normal;
}
.music-attachment-title:has(+ .music-attachment-artist)::after {
content: ' • ';
}
.music-attachment-play, .music-attachment-play,
.music-attachment-remove { .music-attachment-remove {
background: none; background: none;

View File

@ -42,8 +42,13 @@ export default function MusicAttachment({ track, onRemove, showRemove = false })
</div> </div>
<div className="music-attachment-info"> <div className="music-attachment-info">
<div className="music-attachment-title">{track.title}</div> <div className="music-attachment-title">{track.title || 'Unknown Track'}</div>
<div className="music-attachment-artist">{track.artist?.name || 'Unknown'}</div> {track.artist?.name && (
<>
<span style={{ color: 'var(--text-secondary)', margin: '0 4px' }}></span>
<div className="music-attachment-artist">{track.artist.name}</div>
</>
)}
</div> </div>
<button className="music-attachment-play" onClick={handlePlay}> <button className="music-attachment-play" onClick={handlePlay}>

View File

@ -195,13 +195,6 @@ export default function PostCard({ post, currentUser, onUpdate }) {
</div> </div>
)} )}
{/* Прикрепленная музыка */}
{post.attachedTrack && (
<div className="post-music">
<MusicAttachment track={post.attachedTrack} />
</div>
)}
{/* Изображения */} {/* Изображения */}
{images.length > 0 && ( {images.length > 0 && (
<div className="post-images"> <div className="post-images">
@ -258,6 +251,20 @@ export default function PostCard({ post, currentUser, onUpdate }) {
</div> </div>
)} )}
{/* Музыкальный трек (после изображений, компактно) */}
{post.attachedTrack && (
<div className="post-music">
<MusicAttachment track={post.attachedTrack} />
</div>
)}
{/* Прикрепленная музыка (после изображений, компактно) */}
{post.attachedTrack && (
<div className="post-music">
<MusicAttachment track={post.attachedTrack} />
</div>
)}
{/* Теги */} {/* Теги */}
<div className="post-tags"> <div className="post-tags">
{post.tags.map((tag, index) => { {post.tags.map((tag, index) => {

View File

@ -3,36 +3,15 @@ import { Music, User } from 'lucide-react'
import { hapticFeedback } from '../utils/telegram' import { hapticFeedback } from '../utils/telegram'
import './Media.css' import './Media.css'
// Иконка лисы (SVG в стиле аниме) // Иконка лисы из Icons8
const FoxIcon = ({ size = 48 }) => ( const FoxIcon = ({ size = 48 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <img
{/* Голова */} src="https://img.icons8.com/windows/32/fox.png"
<path d="M12 8C15 8 17 10 17 12.5C17 13.5 16.5 14.5 16 15" /> alt="Fox"
<path d="M12 8C9 8 7 10 7 12.5C7 13.5 7.5 14.5 8 15" /> width={size}
height={size}
{/* Уши */} style={{ objectFit: 'contain' }}
<path d="M10 6L9 3L8 6" /> />
<path d="M14 6L15 3L16 6" />
{/* Мордочка */}
<ellipse cx="12" cy="13" rx="4.5" ry="3.5" />
{/* Глаза */}
<circle cx="10" cy="12.5" r="1.2" fill="currentColor" />
<circle cx="14" cy="12.5" r="1.2" fill="currentColor" />
{/* Нос */}
<path d="M12 14.5L11.5 15.5L12 16.5L12.5 15.5Z" fill="currentColor" />
{/* Рот */}
<path d="M10.5 15C11 16 11.5 16.5 12 16.5C12.5 16.5 13 16 13.5 15" />
{/* Туловище */}
<ellipse cx="12" cy="18.5" rx="3.5" ry="2.5" />
{/* Хвост */}
<path d="M15.5 17C17.5 16.5 19 18 19.5 20C20 22 19 23.5 17.5 24" />
</svg>
) )
export default function Media({ user }) { export default function Media({ user }) {
@ -84,7 +63,11 @@ export default function Media({ user }) {
style={{ '--category-color': category.color }} style={{ '--category-color': category.color }}
> >
<div className="media-card-icon"> <div className="media-card-icon">
<Icon size={48} strokeWidth={2} /> {category.id === 'furry' ? (
<Icon size={48} />
) : (
<Icon size={48} strokeWidth={2} />
)}
</div> </div>
<h2 className="media-card-title">{category.name}</h2> <h2 className="media-card-title">{category.name}</h2>
</button> </button>