This commit is contained in:
silverpro89
2025-12-30 20:35:29 +07:00
parent 35800d77d6
commit 781629cd43
9 changed files with 2480 additions and 1 deletions

8
.gitignore vendored
View File

@@ -49,6 +49,14 @@ web_modules/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# Video files and metadata
public/*.mp4
public/*.webm
public/*.avi
public/*.mov
public/*.mkv
metadata/*.json
# Optional npm cache directory # Optional npm cache directory
.npm .npm

247
README.md
View File

@@ -1,2 +1,247 @@
# sena_background_controller # 🎬 Sena Background Controller
Video Background Controller - Hệ thống quản lý video backgrounds với tính năng upload, metadata management và tích hợp Meilisearch.
## 📋 Tính năng
- ✅ Upload video files lên server
- ✅ Lưu trữ video trong folder `public/`
- ✅ Quản lý metadata chi tiết cho mỗi video
- ✅ Tự động tạo file JSON metadata cho Meilisearch
- ✅ Tìm kiếm theo topic, category, theme, colors, tags
- ✅ RESTful API để quản lý videos
- ✅ Giao diện web đẹp và dễ sử dụng
## 🚀 Cài đặt
### 1. Clone repository
```bash
git clone <repository-url>
cd sena_background_controller
```
### 2. Cài đặt dependencies
```bash
npm install
```
### 3. Khởi động server
```bash
npm start
```
Hoặc dùng nodemon để tự động reload:
```bash
npm run dev
```
Server sẽ chạy tại: `http://localhost:3000`
## 📁 Cấu trúc thư mục
```
sena_background_controller/
├── data/ # Data directory (original)
├── public/ # Uploaded video files
├── metadata/ # JSON metadata files
├── src/
│ ├── index.html # Web interface
│ └── server.js # Express server
├── package.json
└── README.md
```
## 🎯 Sử dụng
### 1. Upload Video qua Web Interface
1. Mở trình duyệt và truy cập: `http://localhost:3000`
2. Chọn video file
3. Điền thông tin metadata:
- **Topic**: Chủ đề của video (VD: Nature, Technology, Abstract)
- **Category**: Danh mục (Abstract, Nature, Technology, Urban, etc.)
- **Theme**: Dark hoặc Light
- **Colors**: Chọn các màu nền có trong video (Blue, Green, Yellow, Red, Purple, Orange, Pink, Black, White)
- **Tags**: Các tag bổ sung (phân cách bằng dấu phẩy)
4. Click "Upload Video & Generate Metadata"
### 2. API Endpoints
#### Upload Video
```bash
POST http://localhost:3000/upload
Content-Type: multipart/form-data
Form Data:
- video: [video file]
- topic: "Nature Landscape"
- category: "nature"
- theme: "light"
- colors: ["blue", "green"]
- tags: ["4k", "loop", "smooth"]
```
#### Lấy danh sách tất cả videos
```bash
GET http://localhost:3000/videos
```
#### Lấy thông tin 1 video
```bash
GET http://localhost:3000/video/:id
```
#### Xóa video
```bash
DELETE http://localhost:3000/video/:id
```
#### Lấy tất cả metadata cho Meilisearch
```bash
GET http://localhost:3000/meilisearch/update
```
## 🔍 Tích hợp Meilisearch
### 1. Cài đặt Meilisearch
**Trên Windows:**
```bash
# Download từ GitHub releases
# hoặc dùng curl
curl -L https://install.meilisearch.com | sh
```
**Trên macOS:**
```bash
brew install meilisearch
```
**Trên Linux:**
```bash
curl -L https://install.meilisearch.com | sh
```
### 2. Khởi động Meilisearch
```bash
meilisearch --master-key="YOUR_MASTER_KEY"
```
Meilisearch sẽ chạy tại: `http://localhost:7700`
### 3. Tạo Index và Upload Documents
Sử dụng Meilisearch API hoặc SDK để tạo index và upload metadata:
```javascript
const { MeiliSearch } = require('meilisearch');
const client = new MeiliSearch({
host: 'http://localhost:7700',
apiKey: 'YOUR_MASTER_KEY',
});
// Tạo index
const index = client.index('videos');
// Lấy tất cả metadata từ server
fetch('http://localhost:3000/meilisearch/update')
.then(res => res.json())
.then(data => {
// Upload documents lên Meilisearch
index.addDocuments(data.documents);
});
```
### 4. Cấu hình Searchable Attributes
```javascript
index.updateSearchableAttributes([
'topic',
'category',
'theme',
'colors',
'tags',
'searchableText'
]);
index.updateFilterableAttributes([
'category',
'theme',
'colors'
]);
```
### 5. Tìm kiếm Videos
```javascript
// Tìm kiếm theo text
const results = await index.search('nature blue');
// Tìm kiếm với filters
const results = await index.search('landscape', {
filter: ['theme = dark', 'colors = blue']
});
```
## 📝 Format Metadata JSON
Mỗi video sẽ có 1 file JSON metadata với format sau:
```json
{
"id": "video_1703925601234",
"filename": "nature_landscape_1703925601234.mp4",
"originalName": "nature_landscape.mp4",
"path": "/public/nature_landscape_1703925601234.mp4",
"size": 52428800,
"mimetype": "video/mp4",
"topic": "Nature Landscape",
"category": "nature",
"theme": "light",
"colors": ["blue", "green"],
"tags": ["4k", "loop", "smooth"],
"uploadedAt": "2023-12-30T10:00:01.234Z",
"searchableText": "Nature Landscape nature light blue green 4k loop smooth"
}
```
## 🎨 Danh sách Categories
- Abstract
- Nature
- Technology
- Urban
- Particles
- Gradient
- Geometric
- Animation
- Other
## 🎨 Danh sách Colors
- Blue (🔵)
- Green (🟢)
- Yellow (🟡)
- Red (🔴)
- Purple (🟣)
- Orange (🟠)
- Pink (🌸)
- Black (⚫)
- White (⚪)
## 🛠️ Technologies
- **Backend**: Node.js + Express
- **File Upload**: Multer
- **Search**: Meilisearch
- **Frontend**: HTML5 + CSS3 + Vanilla JavaScript
## 📄 License
ISC

BIN
data/13.mp4 Normal file

Binary file not shown.

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "sena_background_controller",
"version": "1.0.0",
"description": "Video background controller with upload, metadata management and Meilisearch integration",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"keywords": [
"video",
"upload",
"meilisearch",
"background",
"metadata"
],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}

630
src/gallery.html Normal file
View File

@@ -0,0 +1,630 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Gallery - Background Controller</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
margin-bottom: 30px;
}
h1 {
color: #333;
margin-bottom: 20px;
font-size: 2.5em;
text-align: center;
}
.nav-links {
display: flex;
gap: 15px;
justify-content: center;
margin-bottom: 20px;
}
.nav-links a {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: transform 0.2s;
}
.nav-links a:hover {
transform: translateY(-2px);
}
.filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.filter-group {
display: flex;
flex-direction: column;
}
.filter-group label {
font-weight: 600;
margin-bottom: 5px;
color: #555;
}
.filter-group input,
.filter-group select {
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
}
.stats {
display: flex;
gap: 20px;
justify-content: center;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 10px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 12px;
color: #666;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
margin-top: 20px;
}
.video-card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
transition: transform 0.3s, box-shadow 0.3s;
}
.video-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
}
.video-wrapper {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
background: #000;
overflow: hidden;
}
.video-wrapper video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.video-info {
padding: 20px;
}
.video-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.video-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.meta-tag {
padding: 5px 12px;
background: #f0f4ff;
color: #667eea;
border-radius: 15px;
font-size: 12px;
font-weight: 600;
}
.meta-tag.category {
background: #e8f5e9;
color: #4caf50;
}
.meta-tag.theme-dark {
background: #333;
color: white;
}
.meta-tag.theme-light {
background: #fff3e0;
color: #ff9800;
}
.color-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
}
.color-dot {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.color-blue { background: #2196F3; }
.color-green { background: #4CAF50; }
.color-yellow { background: #FFEB3B; }
.color-red { background: #F44336; }
.color-purple { background: #9C27B0; }
.color-orange { background: #FF9800; }
.color-pink { background: #E91E63; }
.color-black { background: #000000; }
.color-white { background: #FFFFFF; border-color: #ddd; }
.video-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 15px;
}
.tag {
padding: 3px 10px;
background: #e0e0e0;
border-radius: 10px;
font-size: 11px;
color: #555;
}
.video-actions {
display: flex;
gap: 10px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-view {
background: #667eea;
color: white;
}
.btn-view:hover {
background: #5568d3;
}
.btn-delete {
background: #f44336;
color: white;
}
.btn-delete:hover {
background: #d32f2f;
}
.video-date {
font-size: 12px;
color: #999;
margin-top: 10px;
}
.loading {
text-align: center;
padding: 50px;
color: white;
font-size: 20px;
}
.no-videos {
text-align: center;
padding: 50px;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.no-videos h2 {
color: #999;
margin-bottom: 20px;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
overflow: auto;
}
.modal-content {
position: relative;
margin: 50px auto;
max-width: 1200px;
width: 90%;
}
.close-modal {
position: absolute;
top: -40px;
right: 0;
color: white;
font-size: 40px;
font-weight: bold;
cursor: pointer;
z-index: 1001;
}
.close-modal:hover {
color: #ddd;
}
.modal video {
width: 100%;
max-height: 80vh;
border-radius: 10px;
}
.modal-info {
background: white;
padding: 30px;
border-radius: 10px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎬 Video Gallery</h1>
<div class="nav-links">
<a href="index.html">📤 Upload Video</a>
<a href="gallery.html">🖼️ Gallery</a>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="totalVideos">0</div>
<div class="stat-label">Total Videos</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalSize">0 MB</div>
<div class="stat-label">Total Size</div>
</div>
</div>
<div class="filters">
<div class="filter-group">
<label>🔍 Search</label>
<input type="text" id="searchInput" placeholder="Search by topic, tags...">
</div>
<div class="filter-group">
<label>📂 Category</label>
<select id="categoryFilter">
<option value="">All Categories</option>
<option value="abstract">Abstract</option>
<option value="nature">Nature</option>
<option value="technology">Technology</option>
<option value="urban">Urban</option>
<option value="particles">Particles</option>
<option value="gradient">Gradient</option>
<option value="geometric">Geometric</option>
<option value="animation">Animation</option>
<option value="other">Other</option>
</select>
</div>
<div class="filter-group">
<label>🌓 Theme</label>
<select id="themeFilter">
<option value="">All Themes</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
<div class="filter-group">
<label>🎨 Color</label>
<select id="colorFilter">
<option value="">All Colors</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="yellow">Yellow</option>
<option value="red">Red</option>
<option value="purple">Purple</option>
<option value="orange">Orange</option>
<option value="pink">Pink</option>
<option value="black">Black</option>
<option value="white">White</option>
</select>
</div>
</div>
</div>
<div class="loading" id="loading">⏳ Loading videos...</div>
<div class="gallery" id="gallery"></div>
<div class="no-videos" id="noVideos" style="display: none;">
<h2>📭 No videos found</h2>
<p>Upload your first video to get started!</p>
<a href="index.html" style="display: inline-block; margin-top: 20px; padding: 15px 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-decoration: none; border-radius: 8px; font-weight: 600;">Upload Video</a>
</div>
</div>
<!-- Video Modal -->
<div id="videoModal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="closeModal()">&times;</span>
<video id="modalVideo" controls autoplay></video>
<div class="modal-info" id="modalInfo"></div>
</div>
</div>
<script>
let allVideos = [];
let filteredVideos = [];
// Load videos on page load
window.addEventListener('load', loadVideos);
// Filter listeners
document.getElementById('searchInput').addEventListener('input', applyFilters);
document.getElementById('categoryFilter').addEventListener('change', applyFilters);
document.getElementById('themeFilter').addEventListener('change', applyFilters);
document.getElementById('colorFilter').addEventListener('change', applyFilters);
async function loadVideos() {
try {
const response = await fetch('http://localhost:3000/videos');
const data = await response.json();
allVideos = data.videos || [];
filteredVideos = [...allVideos];
updateStats();
displayVideos();
document.getElementById('loading').style.display = 'none';
} catch (error) {
console.error('Error loading videos:', error);
document.getElementById('loading').innerHTML = '❌ Error loading videos. Make sure the server is running.';
}
}
function updateStats() {
document.getElementById('totalVideos').textContent = allVideos.length;
const totalBytes = allVideos.reduce((sum, video) => sum + (video.size || 0), 0);
const totalMB = (totalBytes / (1024 * 1024)).toFixed(2);
document.getElementById('totalSize').textContent = totalMB + ' MB';
}
function applyFilters() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const category = document.getElementById('categoryFilter').value;
const theme = document.getElementById('themeFilter').value;
const color = document.getElementById('colorFilter').value;
filteredVideos = allVideos.filter(video => {
// Search filter
const matchesSearch = !searchTerm ||
video.topic?.toLowerCase().includes(searchTerm) ||
video.tags?.some(tag => tag.toLowerCase().includes(searchTerm)) ||
video.searchableText?.toLowerCase().includes(searchTerm);
// Category filter
const matchesCategory = !category || video.category === category;
// Theme filter
const matchesTheme = !theme || video.theme === theme;
// Color filter
const matchesColor = !color || video.colors?.includes(color);
return matchesSearch && matchesCategory && matchesTheme && matchesColor;
});
displayVideos();
}
function displayVideos() {
const gallery = document.getElementById('gallery');
const noVideos = document.getElementById('noVideos');
if (filteredVideos.length === 0) {
gallery.innerHTML = '';
noVideos.style.display = 'block';
return;
}
noVideos.style.display = 'none';
gallery.innerHTML = filteredVideos.map(video => `
<div class="video-card">
<div class="video-wrapper">
<video src="http://localhost:3000${video.path}"
preload="metadata"
onmouseover="this.play()"
onmouseout="this.pause(); this.currentTime=0;"
muted
loop>
</video>
</div>
<div class="video-info">
<div class="video-title">${video.topic || 'Untitled'}</div>
<div class="video-meta">
<span class="meta-tag category">${video.category || 'N/A'}</span>
<span class="meta-tag theme-${video.theme}">${video.theme ? '🌓 ' + video.theme : ''}</span>
</div>
${video.colors && video.colors.length > 0 ? `
<div class="color-tags">
${video.colors.map(color => `
<div class="color-dot color-${color}" title="${color}"></div>
`).join('')}
</div>
` : ''}
${video.tags && video.tags.length > 0 ? `
<div class="video-tags">
${video.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
` : ''}
<div class="video-date">
📅 ${new Date(video.uploadedAt).toLocaleDateString('vi-VN')}
${formatFileSize(video.size)}
</div>
<div class="video-actions">
<button class="btn btn-view" onclick='viewVideo(${JSON.stringify(video)})'>
👁️ View
</button>
<button class="btn btn-delete" onclick="deleteVideo('${video.id}')">
🗑️ Delete
</button>
</div>
</div>
</div>
`).join('');
}
function formatFileSize(bytes) {
if (!bytes) return '';
const mb = (bytes / (1024 * 1024)).toFixed(2);
return ` | 💾 ${mb} MB`;
}
function viewVideo(video) {
const modal = document.getElementById('videoModal');
const modalVideo = document.getElementById('modalVideo');
const modalInfo = document.getElementById('modalInfo');
modalVideo.src = `http://localhost:3000${video.path}`;
modalInfo.innerHTML = `
<h2>${video.topic}</h2>
<p><strong>Category:</strong> ${video.category}</p>
<p><strong>Theme:</strong> ${video.theme}</p>
<p><strong>Colors:</strong> ${video.colors?.join(', ') || 'N/A'}</p>
<p><strong>Tags:</strong> ${video.tags?.join(', ') || 'N/A'}</p>
<p><strong>Filename:</strong> ${video.filename}</p>
<p><strong>Size:</strong> ${formatFileSize(video.size)}</p>
<p><strong>Uploaded:</strong> ${new Date(video.uploadedAt).toLocaleString('vi-VN')}</p>
`;
modal.style.display = 'block';
}
function closeModal() {
const modal = document.getElementById('videoModal');
const modalVideo = document.getElementById('modalVideo');
modal.style.display = 'none';
modalVideo.pause();
modalVideo.src = '';
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('videoModal');
if (event.target == modal) {
closeModal();
}
}
async function deleteVideo(videoId) {
if (!confirm('Bạn có chắc muốn xóa video này?')) {
return;
}
try {
const response = await fetch(`http://localhost:3000/video/${videoId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
alert('✅ Đã xóa video thành công!');
loadVideos(); // Reload videos
} else {
alert('❌ Lỗi: ' + result.error);
}
} catch (error) {
alert('❌ Lỗi khi xóa video: ' + error.message);
}
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
</script>
</body>
</html>

380
src/index.html Normal file
View File

@@ -0,0 +1,380 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Background Controller</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
font-size: 2.5em;
}
.upload-section {
margin-bottom: 40px;
padding: 30px;
background: #f8f9fa;
border-radius: 15px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 600;
font-size: 14px;
}
input[type="file"],
input[type="text"],
select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="file"]:focus,
input[type="text"]:focus,
select:focus {
outline: none;
border-color: #667eea;
}
.color-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
}
.color-option {
display: flex;
align-items: center;
padding: 8px;
background: white;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
}
.color-option:hover {
transform: scale(1.05);
}
.color-option input[type="checkbox"] {
margin-right: 8px;
width: 20px;
height: 20px;
cursor: pointer;
}
.theme-group {
display: flex;
gap: 20px;
}
.theme-option {
flex: 1;
padding: 15px;
background: white;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: all 0.3s;
}
.theme-option input[type="radio"] {
margin-right: 8px;
}
.theme-option:has(input:checked) {
border-color: #667eea;
background: #f0f4ff;
}
button {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
.success-message,
.error-message {
padding: 15px;
border-radius: 8px;
margin-top: 20px;
display: none;
}
.success-message {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error-message {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.video-preview {
margin-top: 20px;
max-width: 100%;
}
.video-preview video {
width: 100%;
max-height: 300px;
border-radius: 8px;
}
.metadata-display {
margin-top: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>🎬 Video Background Controller</h1>
<div style="text-align: center; margin-bottom: 20px;">
<a href="gallery.html" style="display: inline-block; padding: 12px 25px; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: transform 0.2s;">
🖼️ Xem Gallery
</a>
</div>
<div class="upload-section">
<form id="uploadForm" enctype="multipart/form-data">
<div class="form-group">
<label>📹 Chọn Video File</label>
<input type="file" id="videoFile" name="video" accept="video/*" required>
</div>
<div class="video-preview" id="videoPreview" style="display: none;">
<video id="previewVideo" controls></video>
</div>
<div class="form-group">
<label>📝 Topic</label>
<input type="text" id="topic" name="topic" placeholder="Ví dụ: Nature, Technology, Abstract..." required>
</div>
<div class="form-group">
<label>📂 Category</label>
<select id="category" name="category" required>
<option value="">-- Chọn Category --</option>
<option value="abstract">Abstract</option>
<option value="nature">Nature</option>
<option value="technology">Technology</option>
<option value="urban">Urban</option>
<option value="particles">Particles</option>
<option value="gradient">Gradient</option>
<option value="geometric">Geometric</option>
<option value="animation">Animation</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label>🌓 Theme</label>
<div class="theme-group">
<label class="theme-option">
<input type="radio" name="theme" value="dark" required>
🌙 Dark
</label>
<label class="theme-option">
<input type="radio" name="theme" value="light" required>
☀️ Light
</label>
</div>
</div>
<div class="form-group">
<label>🎨 Background Colors (chọn nhiều)</label>
<div class="color-group">
<label class="color-option">
<input type="checkbox" name="colors" value="blue">
🔵 Blue
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="green">
🟢 Green
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="yellow">
🟡 Yellow
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="red">
🔴 Red
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="purple">
🟣 Purple
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="orange">
🟠 Orange
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="pink">
🌸 Pink
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="black">
⚫ Black
</label>
<label class="color-option">
<input type="checkbox" name="colors" value="white">
⚪ White
</label>
</div>
</div>
<div class="form-group">
<label>🏷️ Tags (phân cách bằng dấu phẩy)</label>
<input type="text" id="tags" name="tags" placeholder="motion, smooth, loop, 4k...">
</div>
<button type="submit">📤 Upload Video & Generate Metadata</button>
<div class="success-message" id="successMessage"></div>
<div class="error-message" id="errorMessage"></div>
<div class="metadata-display" id="metadataDisplay" style="display: none;"></div>
</form>
</div>
</div>
<script>
const form = document.getElementById('uploadForm');
const videoFile = document.getElementById('videoFile');
const videoPreview = document.getElementById('videoPreview');
const previewVideo = document.getElementById('previewVideo');
const successMessage = document.getElementById('successMessage');
const errorMessage = document.getElementById('errorMessage');
const metadataDisplay = document.getElementById('metadataDisplay');
// Preview video khi chọn file
videoFile.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
previewVideo.src = url;
videoPreview.style.display = 'block';
}
});
// Handle form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
// Hide previous messages
successMessage.style.display = 'none';
errorMessage.style.display = 'none';
metadataDisplay.style.display = 'none';
// Get form data
const formData = new FormData();
formData.append('video', videoFile.files[0]);
formData.append('topic', document.getElementById('topic').value);
formData.append('category', document.getElementById('category').value);
formData.append('theme', document.querySelector('input[name="theme"]:checked').value);
// Get selected colors
const colors = Array.from(document.querySelectorAll('input[name="colors"]:checked'))
.map(cb => cb.value);
formData.append('colors', JSON.stringify(colors));
// Get tags
const tags = document.getElementById('tags').value
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
formData.append('tags', JSON.stringify(tags));
try {
const response = await fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
successMessage.textContent = `✅ Upload thành công! Video: ${result.videoPath}`;
successMessage.style.display = 'block';
// Display metadata
metadataDisplay.textContent = JSON.stringify(result.metadata, null, 2);
metadataDisplay.style.display = 'block';
// Reset form
form.reset();
videoPreview.style.display = 'none';
} else {
errorMessage.textContent = `❌ Lỗi: ${result.error}`;
errorMessage.style.display = 'block';
}
} catch (error) {
errorMessage.textContent = `❌ Lỗi kết nối: ${error.message}`;
errorMessage.style.display = 'block';
}
});
</script>
</body>
</html>

158
src/meilisearch-sync.js Normal file
View File

@@ -0,0 +1,158 @@
const { MeiliSearch } = require('meilisearch');
// Cấu hình Meilisearch client
const client = new MeiliSearch({
host: 'http://localhost:7700',
apiKey: 'YOUR_MASTER_KEY_HERE', // Thay bằng master key của bạn
});
// Tên index
const INDEX_NAME = 'videos';
// Khởi tạo và cấu hình index
async function setupMeilisearch() {
try {
console.log('🔧 Setting up Meilisearch...');
// Tạo hoặc lấy index
const index = client.index(INDEX_NAME);
// Cấu hình searchable attributes
await index.updateSearchableAttributes([
'topic',
'category',
'theme',
'colors',
'tags',
'searchableText',
'originalName',
'filename'
]);
// Cấu hình filterable attributes
await index.updateFilterableAttributes([
'category',
'theme',
'colors',
'tags'
]);
// Cấu hình sortable attributes
await index.updateSortableAttributes([
'uploadedAt',
'size'
]);
console.log('✅ Meilisearch setup completed!');
return index;
} catch (error) {
console.error('❌ Error setting up Meilisearch:', error);
throw error;
}
}
// Sync tất cả videos từ server lên Meilisearch
async function syncVideosToMeilisearch() {
try {
console.log('🔄 Syncing videos to Meilisearch...');
// Lấy tất cả metadata từ server
const response = await fetch('http://localhost:3000/meilisearch/update');
const data = await response.json();
const index = client.index(INDEX_NAME);
// Upload documents lên Meilisearch
const result = await index.addDocuments(data.documents);
console.log(`✅ Synced ${data.count} videos to Meilisearch`);
console.log('Task UID:', result.taskUid);
return result;
} catch (error) {
console.error('❌ Error syncing videos:', error);
throw error;
}
}
// Tìm kiếm videos
async function searchVideos(query, options = {}) {
try {
const index = client.index(INDEX_NAME);
const results = await index.search(query, options);
return results;
} catch (error) {
console.error('❌ Error searching videos:', error);
throw error;
}
}
// Ví dụ: Tìm kiếm với filters
async function exampleSearches() {
console.log('\n📚 Example searches:\n');
// 1. Tìm kiếm basic
console.log('1. Basic search for "nature":');
const result1 = await searchVideos('nature');
console.log(` Found ${result1.hits.length} results`);
// 2. Tìm kiếm với filter theme
console.log('\n2. Search for dark theme videos:');
const result2 = await searchVideos('', {
filter: ['theme = dark']
});
console.log(` Found ${result2.hits.length} results`);
// 3. Tìm kiếm với filter colors
console.log('\n3. Search for videos with blue color:');
const result3 = await searchVideos('', {
filter: ['colors = blue']
});
console.log(` Found ${result3.hits.length} results`);
// 4. Tìm kiếm kết hợp
console.log('\n4. Search "landscape" with dark theme and blue color:');
const result4 = await searchVideos('landscape', {
filter: ['theme = dark AND colors = blue']
});
console.log(` Found ${result4.hits.length} results`);
// 5. Tìm kiếm và sắp xếp
console.log('\n5. All videos sorted by upload date (newest first):');
const result5 = await searchVideos('', {
sort: ['uploadedAt:desc'],
limit: 5
});
console.log(` Found ${result5.hits.length} results`);
}
// Main function
async function main() {
try {
// Setup Meilisearch
await setupMeilisearch();
// Sync videos
await syncVideosToMeilisearch();
// Run example searches
await exampleSearches();
} catch (error) {
console.error('Error:', error);
}
}
// Export functions
module.exports = {
setupMeilisearch,
syncVideosToMeilisearch,
searchVideos,
client,
INDEX_NAME
};
// Chạy nếu file được execute trực tiếp
if (require.main === module) {
main();
}

217
src/server.js Normal file
View File

@@ -0,0 +1,217 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const app = express();
const PORT = 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('src'));
app.use('/public', express.static('public'));
// Configure multer for video upload
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../public');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
// Generate unique filename with timestamp
const timestamp = Date.now();
const ext = path.extname(file.originalname);
const nameWithoutExt = path.basename(file.originalname, ext);
const sanitizedName = nameWithoutExt.replace(/[^a-zA-Z0-9]/g, '_');
cb(null, `${sanitizedName}_${timestamp}${ext}`);
}
});
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
// Accept video files only
if (file.mimetype.startsWith('video/')) {
cb(null, true);
} else {
cb(new Error('Only video files are allowed!'), false);
}
},
limits: {
fileSize: 500 * 1024 * 1024 // 500MB limit
}
});
// Upload endpoint
app.post('/upload', upload.single('video'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No video file uploaded' });
}
// Parse form data
const { topic, category, theme, colors, tags } = req.body;
// Create metadata object
const metadata = {
id: `video_${Date.now()}`,
filename: req.file.filename,
originalName: req.file.originalname,
path: `/public/${req.file.filename}`,
size: req.file.size,
mimetype: req.file.mimetype,
topic: topic,
category: category,
theme: theme,
colors: JSON.parse(colors || '[]'),
tags: JSON.parse(tags || '[]'),
uploadedAt: new Date().toISOString(),
searchableText: `${topic} ${category} ${theme} ${JSON.parse(colors || '[]').join(' ')} ${JSON.parse(tags || '[]').join(' ')}`
};
// Save metadata to JSON file
const metadataDir = path.join(__dirname, '../metadata');
if (!fs.existsSync(metadataDir)) {
fs.mkdirSync(metadataDir, { recursive: true });
}
const metadataFilename = `${path.parse(req.file.filename).name}.json`;
const metadataPath = path.join(metadataDir, metadataFilename);
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
console.log('✅ Video uploaded successfully:', req.file.filename);
console.log('✅ Metadata saved:', metadataFilename);
res.json({
success: true,
message: 'Video uploaded successfully',
videoPath: `/public/${req.file.filename}`,
metadataPath: `/metadata/${metadataFilename}`,
metadata: metadata
});
} catch (error) {
console.error('Error uploading video:', error);
res.status(500).json({ error: error.message });
}
});
// Get all videos with metadata
app.get('/videos', (req, res) => {
try {
const metadataDir = path.join(__dirname, '../metadata');
if (!fs.existsSync(metadataDir)) {
return res.json({ videos: [] });
}
const files = fs.readdirSync(metadataDir);
const videos = files
.filter(file => file.endsWith('.json'))
.map(file => {
const content = fs.readFileSync(path.join(metadataDir, file), 'utf8');
return JSON.parse(content);
})
.sort((a, b) => new Date(b.uploadedAt) - new Date(a.uploadedAt));
res.json({ videos });
} catch (error) {
console.error('Error fetching videos:', error);
res.status(500).json({ error: error.message });
}
});
// Get single video metadata
app.get('/video/:id', (req, res) => {
try {
const metadataDir = path.join(__dirname, '../metadata');
const files = fs.readdirSync(metadataDir);
for (const file of files) {
if (file.endsWith('.json')) {
const content = fs.readFileSync(path.join(metadataDir, file), 'utf8');
const metadata = JSON.parse(content);
if (metadata.id === req.params.id) {
return res.json(metadata);
}
}
}
res.status(404).json({ error: 'Video not found' });
} catch (error) {
console.error('Error fetching video:', error);
res.status(500).json({ error: error.message });
}
});
// Delete video and its metadata
app.delete('/video/:id', (req, res) => {
try {
const metadataDir = path.join(__dirname, '../metadata');
const publicDir = path.join(__dirname, '../public');
const files = fs.readdirSync(metadataDir);
for (const file of files) {
if (file.endsWith('.json')) {
const metadataPath = path.join(metadataDir, file);
const content = fs.readFileSync(metadataPath, 'utf8');
const metadata = JSON.parse(content);
if (metadata.id === req.params.id) {
// Delete video file
const videoPath = path.join(publicDir, metadata.filename);
if (fs.existsSync(videoPath)) {
fs.unlinkSync(videoPath);
}
// Delete metadata file
fs.unlinkSync(metadataPath);
console.log('🗑️ Deleted video:', metadata.filename);
return res.json({ success: true, message: 'Video deleted successfully' });
}
}
}
res.status(404).json({ error: 'Video not found' });
} catch (error) {
console.error('Error deleting video:', error);
res.status(500).json({ error: error.message });
}
});
// Endpoint to get all metadata for Meilisearch sync
app.get('/meilisearch/update', (req, res) => {
try {
const metadataDir = path.join(__dirname, '../metadata');
if (!fs.existsSync(metadataDir)) {
return res.json({ documents: [] });
}
const files = fs.readdirSync(metadataDir);
const documents = files
.filter(file => file.endsWith('.json'))
.map(file => {
const content = fs.readFileSync(path.join(metadataDir, file), 'utf8');
return JSON.parse(content);
});
res.json({ documents, count: documents.length });
} catch (error) {
console.error('Error preparing Meilisearch update:', error);
res.status(500).json({ error: error.message });
}
});
app.listen(PORT, () => {
console.log(`🚀 Server is running on http://localhost:${PORT}`);
console.log(`📁 Upload folder: ${path.join(__dirname, '../public')}`);
console.log(`📄 Metadata folder: ${path.join(__dirname, '../metadata')}`);
});

814
yarn.lock Normal file
View File

@@ -0,0 +1,814 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
dependencies:
mime-types "~2.1.34"
negotiator "0.6.3"
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
append-field@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
body-parser@~1.20.3:
version "1.20.4"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
dependencies:
bytes "~3.1.2"
content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "~1.2.0"
http-errors "~2.0.1"
iconv-lite "~0.4.24"
on-finished "~2.4.1"
qs "~6.14.0"
raw-body "~2.5.3"
type-is "~1.6.18"
unpipe "~1.0.0"
brace-expansion@^1.1.7:
version "1.1.12"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
busboy@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
bytes@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
call-bound@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
dependencies:
call-bind-apply-helpers "^1.0.2"
get-intrinsic "^1.3.0"
chokidar@^3.5.2:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
concat-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
dependencies:
buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
content-disposition@~0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
dependencies:
safe-buffer "5.2.1"
content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
cookie-signature@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
cookie@~0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
debug@^4:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0, destroy@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
encodeurl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
express@^4.18.2:
version "4.22.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069"
integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "~1.20.3"
content-disposition "~0.5.4"
content-type "~1.0.4"
cookie "~0.7.1"
cookie-signature "~1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.3.1"
fresh "~0.5.2"
http-errors "~2.0.0"
merge-descriptors "1.0.3"
methods "~1.1.2"
on-finished "~2.4.1"
parseurl "~1.3.3"
path-to-regexp "~0.1.12"
proxy-addr "~2.0.7"
qs "~6.14.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "~0.19.0"
serve-static "~1.16.2"
setprototypeof "1.2.0"
statuses "~2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
finalhandler@~1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88"
integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==
dependencies:
debug "2.6.9"
encodeurl "~2.0.0"
escape-html "~1.0.3"
on-finished "~2.4.1"
parseurl "~1.3.3"
statuses "~2.0.2"
unpipe "~1.0.0"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
http-errors@~2.0.0, http-errors@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
dependencies:
depd "~2.0.0"
inherits "~2.0.4"
setprototypeof "~1.2.0"
statuses "~2.0.2"
toidentifier "~1.0.1"
iconv-lite@~0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
inherits@^2.0.3, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
merge-descriptors@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mkdirp@^0.5.4:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
dependencies:
minimist "^1.2.6"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.3, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
multer@^1.4.5-lts.1:
version "1.4.5-lts.2"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.2.tgz#340af065d8685dda846ec9e3d7655fcd50afba2d"
integrity sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==
dependencies:
append-field "^1.0.0"
busboy "^1.0.0"
concat-stream "^1.5.2"
mkdirp "^0.5.4"
object-assign "^4.1.1"
type-is "^1.6.4"
xtend "^4.0.0"
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
nodemon@^3.0.2:
version "3.1.11"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.11.tgz#04a54d1e794fbec9d8f6ffd8bf1ba9ea93a756ed"
integrity sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==
dependencies:
chokidar "^3.5.2"
debug "^4"
ignore-by-default "^1.0.1"
minimatch "^3.1.2"
pstree.remy "^1.1.8"
semver "^7.5.3"
simple-update-notifier "^2.0.0"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.5"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.13.3:
version "1.13.4"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
on-finished@~2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@~0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
pstree.remy@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
qs@~6.14.0:
version "6.14.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
dependencies:
side-channel "^1.1.0"
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@~2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2"
integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==
dependencies:
bytes "~3.1.2"
http-errors "~2.0.1"
iconv-lite "~0.4.24"
unpipe "~1.0.0"
readable-stream@^2.2.2:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
safe-buffer@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@^7.5.3:
version "7.7.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
send@~0.19.0, send@~0.19.1:
version "0.19.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29"
integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "~0.5.2"
http-errors "~2.0.1"
mime "1.6.0"
ms "2.1.3"
on-finished "~2.4.1"
range-parser "~1.2.1"
statuses "~2.0.2"
serve-static@~1.16.2:
version "1.16.3"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9"
integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==
dependencies:
encodeurl "~2.0.0"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "~0.19.1"
setprototypeof@1.2.0, setprototypeof@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
side-channel-list@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
dependencies:
es-errors "^1.3.0"
object-inspect "^1.13.3"
side-channel-map@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
dependencies:
call-bound "^1.0.2"
es-errors "^1.3.0"
get-intrinsic "^1.2.5"
object-inspect "^1.13.3"
side-channel-weakmap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
dependencies:
call-bound "^1.0.2"
es-errors "^1.3.0"
get-intrinsic "^1.2.5"
object-inspect "^1.13.3"
side-channel-map "^1.0.1"
side-channel@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
dependencies:
es-errors "^1.3.0"
object-inspect "^1.13.3"
side-channel-list "^1.0.0"
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
simple-update-notifier@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb"
integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==
dependencies:
semver "^7.5.3"
statuses@~2.0.1, statuses@~2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
toidentifier@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
touch@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
type-is@^1.6.4, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
undefsafe@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==