Initial commit: Organize project structure
- Create organized directory structure (src/, docs/, data/, static/, templates/) - Add comprehensive .gitignore for Python projects - Move Python source files to src/ - Move documentation files to docs/ with project/ and user/ subdirectories - Move database files to data/ - Update all database path references in Python code - Maintain Flask static/ and templates/ directories 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
285
src/app.py
Normal file
285
src/app.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
FLASK WEB APPLICATION - INDEX-SISTEM-JOCURI
|
||||
|
||||
Author: Claude AI Assistant
|
||||
Date: 2025-09-09
|
||||
Purpose: Web interface for searching educational activities
|
||||
|
||||
Features:
|
||||
- Search interface matching interfata-web.jpg mockup exactly
|
||||
- 9 filter dropdowns as specified in PRD
|
||||
- Full-text search functionality
|
||||
- Results display in table format
|
||||
- Links to source files
|
||||
- Activity sheet generation
|
||||
|
||||
PRD Requirements:
|
||||
- RF6: Layout identical to mockup
|
||||
- RF7: Search box for free text search
|
||||
- RF8: 9 dropdown filters
|
||||
- RF9: Apply and Reset buttons
|
||||
- RF10: Results table display
|
||||
- RF11: Links to source files
|
||||
"""
|
||||
|
||||
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
||||
from database import DatabaseManager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
|
||||
# Initialize database manager
|
||||
db = DatabaseManager("../data/activities.db")
|
||||
|
||||
# Filter options for dropdowns (based on PRD RF8)
|
||||
FILTER_OPTIONS = {
|
||||
'valori': [
|
||||
'Viziune și perspectivă',
|
||||
'Recunoștință',
|
||||
'Altele',
|
||||
'Management timpul',
|
||||
'Identitate personală'
|
||||
],
|
||||
'durata': [
|
||||
'5-15min',
|
||||
'15-30min',
|
||||
'30+min'
|
||||
],
|
||||
'tematica': [
|
||||
'cercetășesc',
|
||||
'team building',
|
||||
'educativ'
|
||||
],
|
||||
'domeniu': [
|
||||
'sport',
|
||||
'artă',
|
||||
'știință'
|
||||
],
|
||||
'metoda': [
|
||||
'joc',
|
||||
'poveste',
|
||||
'atelier'
|
||||
],
|
||||
'materiale': [
|
||||
'fără',
|
||||
'simple',
|
||||
'complexe'
|
||||
],
|
||||
'competente_fizice': [
|
||||
'fizice',
|
||||
'mentale',
|
||||
'sociale'
|
||||
],
|
||||
'competente_impactate': [
|
||||
'fizice',
|
||||
'mentale',
|
||||
'sociale'
|
||||
],
|
||||
'participanti': [
|
||||
'2-5',
|
||||
'5-10',
|
||||
'10-30',
|
||||
'30+'
|
||||
],
|
||||
'varsta': [
|
||||
'5-8',
|
||||
'8-12',
|
||||
'12-16',
|
||||
'16+'
|
||||
]
|
||||
}
|
||||
|
||||
def get_dynamic_filter_options():
|
||||
"""Get dynamic filter options from database"""
|
||||
try:
|
||||
return db.get_filter_options()
|
||||
except:
|
||||
return {}
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Main search page"""
|
||||
# Get dynamic filter options from database
|
||||
dynamic_filters = get_dynamic_filter_options()
|
||||
|
||||
# Merge with static options
|
||||
all_filters = FILTER_OPTIONS.copy()
|
||||
all_filters.update(dynamic_filters)
|
||||
|
||||
return render_template('index.html', filters=all_filters)
|
||||
|
||||
@app.route('/search', methods=['GET', 'POST'])
|
||||
def search():
|
||||
"""Search activities based on filters and query"""
|
||||
|
||||
# Get search parameters
|
||||
search_query = request.form.get('search_query', '').strip() or request.args.get('q', '').strip()
|
||||
|
||||
# Get filter values
|
||||
filters = {}
|
||||
for filter_name in FILTER_OPTIONS.keys():
|
||||
value = request.form.get(filter_name) or request.args.get(filter_name)
|
||||
if value and value != '':
|
||||
filters[filter_name] = value
|
||||
|
||||
# Map filter names to database fields
|
||||
db_filters = {}
|
||||
if 'tematica' in filters:
|
||||
db_filters['category'] = filters['tematica']
|
||||
if 'varsta' in filters:
|
||||
db_filters['age_group'] = filters['varsta'] + ' ani'
|
||||
if 'participanti' in filters:
|
||||
db_filters['participants'] = filters['participanti'] + ' persoane'
|
||||
if 'durata' in filters:
|
||||
db_filters['duration'] = filters['durata']
|
||||
if 'materiale' in filters:
|
||||
material_map = {'fără': 'Fără materiale', 'simple': 'Materiale simple', 'complexe': 'Materiale complexe'}
|
||||
db_filters['materials'] = material_map.get(filters['materiale'], filters['materiale'])
|
||||
|
||||
# Search in database
|
||||
try:
|
||||
results = db.search_activities(
|
||||
search_text=search_query if search_query else None,
|
||||
**db_filters,
|
||||
limit=100
|
||||
)
|
||||
|
||||
# Convert results to list of dicts for template
|
||||
activities = []
|
||||
for result in results:
|
||||
activities.append({
|
||||
'id': result['id'],
|
||||
'title': result['title'],
|
||||
'description': result['description'][:200] + '...' if len(result['description']) > 200 else result['description'],
|
||||
'category': result['category'],
|
||||
'age_group': result['age_group'],
|
||||
'participants': result['participants'],
|
||||
'duration': result['duration'],
|
||||
'materials': result['materials'],
|
||||
'file_path': result['file_path'],
|
||||
'tags': json.loads(result['tags']) if result['tags'] else []
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Search error: {e}")
|
||||
activities = []
|
||||
|
||||
# Get dynamic filter options for the form
|
||||
dynamic_filters = get_dynamic_filter_options()
|
||||
all_filters = FILTER_OPTIONS.copy()
|
||||
all_filters.update(dynamic_filters)
|
||||
|
||||
return render_template('results.html',
|
||||
activities=activities,
|
||||
search_query=search_query,
|
||||
applied_filters=filters,
|
||||
filters=all_filters,
|
||||
results_count=len(activities))
|
||||
|
||||
@app.route('/generate_sheet/<int:activity_id>')
|
||||
def generate_sheet(activity_id):
|
||||
"""Generate activity sheet for specific activity"""
|
||||
try:
|
||||
# Get activity from database
|
||||
results = db.search_activities(limit=1000) # Get all to find by ID
|
||||
activity_data = None
|
||||
|
||||
for result in results:
|
||||
if result['id'] == activity_id:
|
||||
activity_data = result
|
||||
break
|
||||
|
||||
if not activity_data:
|
||||
return "Activity not found", 404
|
||||
|
||||
# Get similar activities for recommendations
|
||||
similar_activities = db.search_activities(
|
||||
category=activity_data['category'],
|
||||
limit=5
|
||||
)
|
||||
|
||||
# Filter out current activity and limit to 3
|
||||
recommendations = [act for act in similar_activities if act['id'] != activity_id][:3]
|
||||
|
||||
return render_template('fisa.html',
|
||||
activity=activity_data,
|
||||
recommendations=recommendations)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Sheet generation error: {e}")
|
||||
return f"Error generating sheet: {e}", 500
|
||||
|
||||
@app.route('/file/<path:filename>')
|
||||
def view_file(filename):
|
||||
"""Serve activity files (PDFs, docs, etc.)"""
|
||||
# Security: only serve files from the base directory
|
||||
base_path = Path("/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri")
|
||||
file_path = base_path / filename
|
||||
|
||||
try:
|
||||
if file_path.exists() and file_path.is_file():
|
||||
# For now, just return file info - in production you'd serve the actual file
|
||||
return f"File: {filename}<br>Path: {file_path}<br>Size: {file_path.stat().st_size} bytes"
|
||||
else:
|
||||
return "File not found", 404
|
||||
except Exception as e:
|
||||
return f"Error accessing file: {e}", 500
|
||||
|
||||
@app.route('/api/statistics')
|
||||
def api_statistics():
|
||||
"""API endpoint for database statistics"""
|
||||
try:
|
||||
stats = db.get_statistics()
|
||||
return jsonify(stats)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/reset_filters')
|
||||
def reset_filters():
|
||||
"""Reset all filters and redirect to main page"""
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
return render_template('500.html'), 500
|
||||
|
||||
def init_app():
|
||||
"""Initialize application"""
|
||||
print("🚀 Starting INDEX-SISTEM-JOCURI Flask Application")
|
||||
print("=" * 50)
|
||||
|
||||
# Check if database exists and has data
|
||||
try:
|
||||
stats = db.get_statistics()
|
||||
print(f"✅ Database connected: {stats['total_activities']} activities loaded")
|
||||
|
||||
if stats['total_activities'] == 0:
|
||||
print("⚠️ Warning: No activities found in database!")
|
||||
print(" Run: python indexer.py --clear-db to index files first")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Database error: {e}")
|
||||
|
||||
print(f"🌐 Web interface will be available at: http://localhost:5000")
|
||||
print("📱 Interface matches: interfata-web.jpg")
|
||||
print("=" * 50)
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_app()
|
||||
|
||||
# Run Flask app
|
||||
app.run(
|
||||
host='0.0.0.0', # Accept connections from any IP
|
||||
port=5000,
|
||||
debug=True, # Enable debug mode for development
|
||||
threaded=True # Handle multiple requests
|
||||
)
|
||||
329
src/database.py
Normal file
329
src/database.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DATABASE HELPER - SQLite database management for INDEX-SISTEM-JOCURI
|
||||
|
||||
Author: Claude AI Assistant
|
||||
Date: 2025-09-09
|
||||
Purpose: Database management according to PRD specifications
|
||||
|
||||
Schema based on PRD.md section 5.3
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
class DatabaseManager:
|
||||
"""Manager for SQLite database operations"""
|
||||
|
||||
def __init__(self, db_path: str = "../data/activities.db"):
|
||||
self.db_path = Path(db_path)
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
"""Initialize database with PRD-compliant schema"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Main activities table (PRD Section 5.3)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_type TEXT,
|
||||
page_number INTEGER,
|
||||
tags TEXT,
|
||||
category TEXT,
|
||||
age_group TEXT,
|
||||
participants TEXT,
|
||||
duration TEXT,
|
||||
materials TEXT,
|
||||
difficulty TEXT DEFAULT 'mediu',
|
||||
source_text TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Full-text search table (PRD Section 5.3)
|
||||
cursor.execute('''
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS activities_fts USING fts5(
|
||||
title, description, source_text,
|
||||
content='activities'
|
||||
)
|
||||
''')
|
||||
|
||||
# Search history table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS search_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
query TEXT NOT NULL,
|
||||
results_count INTEGER,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# File processing log
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS file_processing_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_type TEXT,
|
||||
status TEXT,
|
||||
activities_extracted INTEGER DEFAULT 0,
|
||||
error_message TEXT,
|
||||
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"✅ Database initialized: {self.db_path}")
|
||||
|
||||
def insert_activity(self, activity_data: Dict) -> int:
|
||||
"""Insert a new activity into the database"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Convert lists to JSON strings
|
||||
tags_json = json.dumps(activity_data.get('tags', []), ensure_ascii=False)
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO activities
|
||||
(title, description, file_path, file_type, page_number, tags,
|
||||
category, age_group, participants, duration, materials,
|
||||
difficulty, source_text)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
activity_data['title'],
|
||||
activity_data.get('description', ''),
|
||||
activity_data['file_path'],
|
||||
activity_data.get('file_type', ''),
|
||||
activity_data.get('page_number'),
|
||||
tags_json,
|
||||
activity_data.get('category', ''),
|
||||
activity_data.get('age_group', ''),
|
||||
activity_data.get('participants', ''),
|
||||
activity_data.get('duration', ''),
|
||||
activity_data.get('materials', ''),
|
||||
activity_data.get('difficulty', 'mediu'),
|
||||
activity_data.get('source_text', '')
|
||||
))
|
||||
|
||||
activity_id = cursor.lastrowid
|
||||
|
||||
# Update FTS index
|
||||
cursor.execute('''
|
||||
INSERT INTO activities_fts (rowid, title, description, source_text)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (
|
||||
activity_id,
|
||||
activity_data['title'],
|
||||
activity_data.get('description', ''),
|
||||
activity_data.get('source_text', '')
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return activity_id
|
||||
|
||||
def search_activities(self,
|
||||
search_text: str = None,
|
||||
category: str = None,
|
||||
age_group: str = None,
|
||||
participants: str = None,
|
||||
duration: str = None,
|
||||
materials: str = None,
|
||||
difficulty: str = None,
|
||||
limit: int = 50) -> List[Dict]:
|
||||
"""Search activities with multiple filters"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build the query
|
||||
if search_text:
|
||||
# Use FTS for text search
|
||||
query = '''
|
||||
SELECT a.* FROM activities a
|
||||
JOIN activities_fts fts ON a.id = fts.rowid
|
||||
WHERE activities_fts MATCH ?
|
||||
'''
|
||||
params = [search_text]
|
||||
else:
|
||||
query = 'SELECT * FROM activities WHERE 1=1'
|
||||
params = []
|
||||
|
||||
# Add filters
|
||||
if category:
|
||||
query += ' AND category = ?'
|
||||
params.append(category)
|
||||
if age_group:
|
||||
query += ' AND age_group = ?'
|
||||
params.append(age_group)
|
||||
if participants:
|
||||
query += ' AND participants = ?'
|
||||
params.append(participants)
|
||||
if duration:
|
||||
query += ' AND duration = ?'
|
||||
params.append(duration)
|
||||
if materials:
|
||||
query += ' AND materials = ?'
|
||||
params.append(materials)
|
||||
if difficulty:
|
||||
query += ' AND difficulty = ?'
|
||||
params.append(difficulty)
|
||||
|
||||
query += f' LIMIT {limit}'
|
||||
|
||||
cursor.execute(query, params)
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
return results
|
||||
|
||||
def get_filter_options(self) -> Dict[str, List[str]]:
|
||||
"""Get all unique values for filter dropdowns"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
filters = {}
|
||||
|
||||
# Get unique values for each filter field
|
||||
fields = ['category', 'age_group', 'participants', 'duration',
|
||||
'materials', 'difficulty']
|
||||
|
||||
for field in fields:
|
||||
cursor.execute(f'''
|
||||
SELECT DISTINCT {field}
|
||||
FROM activities
|
||||
WHERE {field} IS NOT NULL AND {field} != ''
|
||||
ORDER BY {field}
|
||||
''')
|
||||
filters[field] = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
return filters
|
||||
|
||||
def log_file_processing(self, file_path: str, file_type: str,
|
||||
status: str, activities_count: int = 0,
|
||||
error_message: str = None):
|
||||
"""Log file processing results"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO file_processing_log
|
||||
(file_path, file_type, status, activities_extracted, error_message)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (file_path, file_type, status, activities_count, error_message))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get database statistics"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Total activities
|
||||
cursor.execute('SELECT COUNT(*) FROM activities')
|
||||
total_activities = cursor.fetchone()[0]
|
||||
|
||||
# Activities by category
|
||||
cursor.execute('''
|
||||
SELECT category, COUNT(*)
|
||||
FROM activities
|
||||
WHERE category IS NOT NULL AND category != ''
|
||||
GROUP BY category
|
||||
ORDER BY COUNT(*) DESC
|
||||
''')
|
||||
categories = dict(cursor.fetchall())
|
||||
|
||||
# Activities by age group
|
||||
cursor.execute('''
|
||||
SELECT age_group, COUNT(*)
|
||||
FROM activities
|
||||
WHERE age_group IS NOT NULL AND age_group != ''
|
||||
GROUP BY age_group
|
||||
ORDER BY age_group
|
||||
''')
|
||||
age_groups = dict(cursor.fetchall())
|
||||
|
||||
# Recent file processing
|
||||
cursor.execute('''
|
||||
SELECT file_type, COUNT(*) as processed_files,
|
||||
SUM(activities_extracted) as total_activities
|
||||
FROM file_processing_log
|
||||
GROUP BY file_type
|
||||
ORDER BY COUNT(*) DESC
|
||||
''')
|
||||
file_stats = [dict(zip(['file_type', 'files_processed', 'activities_extracted'], row))
|
||||
for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
'total_activities': total_activities,
|
||||
'categories': categories,
|
||||
'age_groups': age_groups,
|
||||
'file_statistics': file_stats,
|
||||
'last_updated': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def clear_database(self):
|
||||
"""Clear all activities (for re-indexing)"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('DELETE FROM activities')
|
||||
cursor.execute('DELETE FROM activities_fts')
|
||||
cursor.execute('DELETE FROM file_processing_log')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✅ Database cleared for re-indexing")
|
||||
|
||||
def test_database():
|
||||
"""Test database functionality"""
|
||||
db = DatabaseManager("../data/test_activities.db")
|
||||
|
||||
# Test insert
|
||||
test_activity = {
|
||||
'title': 'Test Activity',
|
||||
'description': 'A test activity for validation',
|
||||
'file_path': '/path/to/test.pdf',
|
||||
'file_type': 'pdf',
|
||||
'page_number': 1,
|
||||
'tags': ['test', 'example'],
|
||||
'category': 'Test Category',
|
||||
'age_group': '8-12 ani',
|
||||
'participants': '5-10 persoane',
|
||||
'duration': '15-30 minute',
|
||||
'materials': 'Fără materiale',
|
||||
'source_text': 'Test activity for validation purposes'
|
||||
}
|
||||
|
||||
activity_id = db.insert_activity(test_activity)
|
||||
print(f"✅ Inserted test activity with ID: {activity_id}")
|
||||
|
||||
# Test search
|
||||
results = db.search_activities(search_text="test")
|
||||
print(f"✅ Search found {len(results)} activities")
|
||||
|
||||
# Test filters
|
||||
filters = db.get_filter_options()
|
||||
print(f"✅ Available filters: {list(filters.keys())}")
|
||||
|
||||
# Test statistics
|
||||
stats = db.get_statistics()
|
||||
print(f"✅ Statistics: {stats['total_activities']} total activities")
|
||||
|
||||
print("🎯 Database testing completed successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_database()
|
||||
502
src/game_library_manager.py
Normal file
502
src/game_library_manager.py
Normal file
@@ -0,0 +1,502 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
GAME LIBRARY MANAGER - Manager pentru Colecția de Jocuri și Activități
|
||||
|
||||
Autor: Claude AI Assistant
|
||||
Data: 2025-09-09
|
||||
Scopul: Automatizarea căutărilor și generarea de fișe de activități din colecția catalogată
|
||||
|
||||
Funcționalități:
|
||||
- Căutare activități după criterii multiple
|
||||
- Generare fișe de activități personalizate
|
||||
- Export în format PDF/HTML/Markdown
|
||||
- Statistici și rapoarte
|
||||
- Administrare index
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Tuple, Optional, Union
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
|
||||
@dataclass
|
||||
class Activity:
|
||||
"""Clasă pentru stocarea informațiilor despre o activitate"""
|
||||
id: str
|
||||
title: str
|
||||
file_path: str
|
||||
category: str
|
||||
subcategory: str
|
||||
age_group: str
|
||||
participants: str
|
||||
duration: str
|
||||
materials: str
|
||||
description: str
|
||||
examples: List[str]
|
||||
keywords: List[str]
|
||||
difficulty: str = "mediu"
|
||||
language: str = "ro"
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
return asdict(self)
|
||||
|
||||
def matches_criteria(self, criteria: Dict) -> bool:
|
||||
"""Verifică dacă activitatea se potrivește cu criteriile de căutare"""
|
||||
for key, value in criteria.items():
|
||||
if key == 'age_min' and value:
|
||||
# Extrage vârsta minimă din age_group
|
||||
age_match = re.search(r'(\d+)', self.age_group)
|
||||
if age_match and int(age_match.group(1)) < value:
|
||||
return False
|
||||
elif key == 'keywords' and value:
|
||||
# Caută cuvinte cheie în toate câmpurile text
|
||||
search_text = f"{self.title} {self.description} {' '.join(self.keywords)}".lower()
|
||||
if not any(keyword.lower() in search_text for keyword in value):
|
||||
return False
|
||||
elif key in ['category', 'difficulty', 'language'] and value:
|
||||
if getattr(self, key).lower() != value.lower():
|
||||
return False
|
||||
return True
|
||||
|
||||
class GameLibraryManager:
|
||||
"""Manager principal pentru colecția de jocuri"""
|
||||
|
||||
def __init__(self, base_path: str = "/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri"):
|
||||
self.base_path = Path(base_path)
|
||||
self.index_path = self.base_path / "INDEX-SISTEM-JOCURI"
|
||||
self.db_path = self.index_path / "game_library.db"
|
||||
self.activities: List[Activity] = []
|
||||
self.init_database()
|
||||
self.load_activities_from_index()
|
||||
|
||||
def init_database(self):
|
||||
"""Inițializează baza de date SQLite"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
subcategory TEXT,
|
||||
age_group TEXT,
|
||||
participants TEXT,
|
||||
duration TEXT,
|
||||
materials TEXT,
|
||||
description TEXT,
|
||||
examples TEXT,
|
||||
keywords TEXT,
|
||||
difficulty TEXT DEFAULT 'mediu',
|
||||
language TEXT DEFAULT 'ro',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS search_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
query TEXT NOT NULL,
|
||||
results_count INTEGER,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def load_activities_from_index(self):
|
||||
"""Încarcă activitățile din indexul principal"""
|
||||
# Date structurate din INDEX_MASTER_JOCURI_ACTIVITATI.md
|
||||
sample_activities = [
|
||||
Activity(
|
||||
id="cubs_acting_01",
|
||||
title="Animal Mimes",
|
||||
file_path="./Activities and Games Scouts NZ/Cubs Acting Games.pdf",
|
||||
category="Jocuri Cercetășești",
|
||||
subcategory="Acting Games",
|
||||
age_group="8-11 ani",
|
||||
participants="8-30 copii",
|
||||
duration="5-10 minute",
|
||||
materials="Fără materiale",
|
||||
description="Joc de imitare animale prin mimică, dezvoltă creativitatea și expresia corporală",
|
||||
examples=["Imitarea unui leu", "Mișcarea unei broaște", "Zborul unei păsări"],
|
||||
keywords=["acting", "mimică", "animale", "creativitate", "expresie"]
|
||||
),
|
||||
Activity(
|
||||
id="cubs_team_01",
|
||||
title="Relay Races",
|
||||
file_path="./Activities and Games Scouts NZ/Cubs Team Games.pdf",
|
||||
category="Jocuri Cercetășești",
|
||||
subcategory="Team Games",
|
||||
age_group="8-11 ani",
|
||||
participants="12-30 copii",
|
||||
duration="15-25 minute",
|
||||
materials="Echipament sportiv de bază",
|
||||
description="Curse de ștafetă variate pentru dezvoltarea spiritului de echipă",
|
||||
examples=["Ștafeta cu mingea", "Ștafeta cu obstacole", "Ștafeta cu sacii"],
|
||||
keywords=["ștafetă", "echipă", "alergare", "competiție", "sport"]
|
||||
),
|
||||
Activity(
|
||||
id="teambuilding_01",
|
||||
title="Cercul Încrederii",
|
||||
file_path="./160-de-activitati-dinamice-jocuri-pentru-team-building-.pdf",
|
||||
category="Team Building",
|
||||
subcategory="Exerciții de Încredere",
|
||||
age_group="12+ ani",
|
||||
participants="8-15 persoane",
|
||||
duration="15-20 minute",
|
||||
materials="Niciuna",
|
||||
description="Participanții stau în cerc, unul în mijloc se lasă să cadă încredințându-se în ceilalți",
|
||||
examples=["Căderea încrederii", "Sprijinirea în grup", "Construirea încrederii"],
|
||||
keywords=["încredere", "echipă", "cooperare", "siguranță", "grup"]
|
||||
),
|
||||
Activity(
|
||||
id="escape_room_01",
|
||||
title="Puzzle cu Puncte și Coduri",
|
||||
file_path="./escape-room/101 Puzzles for Low Cost Escape Rooms.pdf",
|
||||
category="Escape Room",
|
||||
subcategory="Coduri și Cifre",
|
||||
age_group="10+ ani",
|
||||
participants="3-8 persoane",
|
||||
duration="10-30 minute",
|
||||
materials="Hârtie, creioane, obiecte cu puncte",
|
||||
description="Puzzle-uri cu puncte care formează coduri numerice sau literale",
|
||||
examples=["Conectarea punctelor pentru cifre", "Coduri Morse cu puncte", "Desene cu sens ascuns"],
|
||||
keywords=["puzzle", "coduri", "logică", "rezolvare probleme", "mister"]
|
||||
),
|
||||
Activity(
|
||||
id="orienteering_01",
|
||||
title="Compass Game cu 8 Posturi",
|
||||
file_path="./Compass Game Beginner.pdf",
|
||||
category="Orientare",
|
||||
subcategory="Jocuri cu Busola",
|
||||
age_group="10+ ani",
|
||||
participants="6-20 persoane",
|
||||
duration="45-90 minute",
|
||||
materials="Busole, conuri colorate, carduri cu provocări",
|
||||
description="Joc cu 8 posturi și 90 de provocări diferite pentru învățarea orientării",
|
||||
examples=["Găsirea azimutului", "Calcularea distanței", "Identificarea pe hartă"],
|
||||
keywords=["orientare", "busola", "azimut", "hartă", "navigare"]
|
||||
),
|
||||
Activity(
|
||||
id="first_aid_01",
|
||||
title="RCP - Resuscitare Cardio-Pulmonară",
|
||||
file_path="./prim-ajutor/RCP_demonstration.jpg",
|
||||
category="Primul Ajutor",
|
||||
subcategory="Tehnici de Salvare",
|
||||
age_group="14+ ani",
|
||||
participants="5-15 persoane",
|
||||
duration="30-45 minute",
|
||||
materials="Manechin RCP, kit primul ajutor",
|
||||
description="Învățarea tehnicilor de RCP pentru situații de urgență",
|
||||
examples=["Compresia toracică", "Ventilația artificială", "Verificarea pulsului"],
|
||||
keywords=["RCP", "primul ajutor", "urgență", "salvare", "resuscitare"],
|
||||
difficulty="avansat"
|
||||
),
|
||||
Activity(
|
||||
id="science_biology_01",
|
||||
title="Leaf Collection & Identification",
|
||||
file_path="./dragon.sleepdeprived.ca/program/science/biology.html",
|
||||
category="Activități Educaționale",
|
||||
subcategory="Biologie",
|
||||
age_group="8+ ani",
|
||||
participants="5-25 persoane",
|
||||
duration="60-120 minute",
|
||||
materials="Pungi pentru colectat, lupă, ghid identificare",
|
||||
description="Colectarea și identificarea frunzelor pentru crearea unui ierbar",
|
||||
examples=["Colectarea frunzelor", "Identificarea speciilor", "Crearea ierbarului"],
|
||||
keywords=["biologie", "natură", "frunze", "identificare", "ierbar"]
|
||||
),
|
||||
Activity(
|
||||
id="songs_welcome_01",
|
||||
title="Welcome Circle Song",
|
||||
file_path="./dragon.sleepdeprived.ca/songbook/songs1/welcome.html",
|
||||
category="Resurse Speciale",
|
||||
subcategory="Cântece de Bun Venit",
|
||||
age_group="5+ ani",
|
||||
participants="8-50 persoane",
|
||||
duration="3-5 minute",
|
||||
materials="Niciuna",
|
||||
description="Cântec simplu în cerc pentru întâmpinarea participanților noi",
|
||||
examples=["Cântecul de bun venit", "Prezentarea numelor", "Formarea cercului"],
|
||||
keywords=["cântec", "bun venit", "cerc", "prezentare", "început"]
|
||||
)
|
||||
]
|
||||
|
||||
self.activities = sample_activities
|
||||
self.save_activities_to_db()
|
||||
|
||||
def save_activities_to_db(self):
|
||||
"""Salvează activitățile în baza de date"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
for activity in self.activities:
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO activities
|
||||
(id, title, file_path, category, subcategory, age_group, participants,
|
||||
duration, materials, description, examples, keywords, difficulty, language)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
activity.id, activity.title, activity.file_path, activity.category,
|
||||
activity.subcategory, activity.age_group, activity.participants,
|
||||
activity.duration, activity.materials, activity.description,
|
||||
json.dumps(activity.examples, ensure_ascii=False),
|
||||
json.dumps(activity.keywords, ensure_ascii=False),
|
||||
activity.difficulty, activity.language
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def search_activities(self, **criteria) -> List[Activity]:
|
||||
"""
|
||||
Caută activități după criterii
|
||||
|
||||
Criterii disponibile:
|
||||
- category: categoria principală
|
||||
- age_min: vârsta minimă
|
||||
- participants_max: numărul maxim de participanți
|
||||
- duration_max: durata maximă în minute
|
||||
- materials: tipul de materiale
|
||||
- keywords: lista de cuvinte cheie
|
||||
- difficulty: nivelul de dificultate
|
||||
"""
|
||||
results = []
|
||||
for activity in self.activities:
|
||||
if activity.matches_criteria(criteria):
|
||||
results.append(activity)
|
||||
|
||||
# Salvează căutarea în istoric
|
||||
self.save_search_to_history(str(criteria), len(results))
|
||||
|
||||
return results
|
||||
|
||||
def save_search_to_history(self, query: str, results_count: int):
|
||||
"""Salvează căutarea în istoricul de căutări"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
'INSERT INTO search_history (query, results_count) VALUES (?, ?)',
|
||||
(query, results_count)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def generate_activity_sheet(self, activity: Activity, format: str = "markdown") -> str:
|
||||
"""Generează o fișă de activitate în formatul specificat"""
|
||||
if format == "markdown":
|
||||
return self._generate_markdown_sheet(activity)
|
||||
elif format == "html":
|
||||
return self._generate_html_sheet(activity)
|
||||
else:
|
||||
raise ValueError(f"Format nepermis: {format}")
|
||||
|
||||
def _generate_markdown_sheet(self, activity: Activity) -> str:
|
||||
"""Generează fișa în format Markdown"""
|
||||
examples_text = "\n".join(f"- {ex}" for ex in activity.examples)
|
||||
keywords_text = ", ".join(activity.keywords)
|
||||
|
||||
sheet = f"""# FIȘA ACTIVITĂȚII: {activity.title}
|
||||
|
||||
## 📋 INFORMAȚII GENERALE
|
||||
- **Categorie:** {activity.category} → {activity.subcategory}
|
||||
- **Grupa de vârstă:** {activity.age_group}
|
||||
- **Numărul participanților:** {activity.participants}
|
||||
- **Durata estimată:** {activity.duration}
|
||||
- **Nivel de dificultate:** {activity.difficulty.capitalize()}
|
||||
|
||||
## 🎯 DESCRIEREA ACTIVITĂȚII
|
||||
{activity.description}
|
||||
|
||||
## 🧰 MATERIALE NECESARE
|
||||
{activity.materials}
|
||||
|
||||
## 💡 EXEMPLE DE APLICARE
|
||||
{examples_text}
|
||||
|
||||
## 🔗 SURSA
|
||||
**Fișier:** `{activity.file_path}`
|
||||
|
||||
## 🏷️ CUVINTE CHEIE
|
||||
{keywords_text}
|
||||
|
||||
---
|
||||
**Generat automat:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||
**ID Activitate:** {activity.id}
|
||||
"""
|
||||
return sheet
|
||||
|
||||
def _generate_html_sheet(self, activity: Activity) -> str:
|
||||
"""Generează fișa în format HTML"""
|
||||
examples_html = "".join(f"<li>{ex}</li>" for ex in activity.examples)
|
||||
keywords_html = ", ".join(f'<span class="keyword">{kw}</span>' for kw in activity.keywords)
|
||||
|
||||
sheet = f"""<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fișa Activității: {activity.title}</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }}
|
||||
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; }}
|
||||
.info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0; }}
|
||||
.info-item {{ background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #667eea; }}
|
||||
.examples {{ background: #e8f5e8; padding: 15px; border-radius: 8px; }}
|
||||
.keyword {{ background: #667eea; color: white; padding: 3px 8px; border-radius: 15px; font-size: 0.9em; }}
|
||||
.footer {{ margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🎮 {activity.title}</h1>
|
||||
<p><strong>{activity.category}</strong> → {activity.subcategory}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<strong>👥 Participanți:</strong><br>{activity.participants}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>⏰ Durata:</strong><br>{activity.duration}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>🎂 Vârsta:</strong><br>{activity.age_group}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>📊 Dificultate:</strong><br>{activity.difficulty.capitalize()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<h3>🎯 Descrierea Activității</h3>
|
||||
<p>{activity.description}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<h3>🧰 Materiale Necesare</h3>
|
||||
<p>{activity.materials}</p>
|
||||
</div>
|
||||
|
||||
<div class="examples">
|
||||
<h3>💡 Exemple de Aplicare</h3>
|
||||
<ul>{examples_html}</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<h3>🏷️ Cuvinte Cheie</h3>
|
||||
<p>{keywords_html}</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>Sursa:</strong> <code>{activity.file_path}</code></p>
|
||||
<p><strong>ID:</strong> {activity.id} | <strong>Generat:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
return sheet
|
||||
|
||||
def export_search_results(self, activities: List[Activity], filename: str, format: str = "markdown"):
|
||||
"""Exportă rezultatele căutării într-un fișier"""
|
||||
output_path = self.index_path / f"{filename}.{format}"
|
||||
|
||||
if format == "markdown":
|
||||
content = f"# REZULTATE CĂUTARE - {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
|
||||
content += f"**Numărul de activități găsite:** {len(activities)}\n\n---\n\n"
|
||||
|
||||
for i, activity in enumerate(activities, 1):
|
||||
content += f"## {i}. {activity.title}\n\n"
|
||||
content += f"**Categorie:** {activity.category} → {activity.subcategory} \n"
|
||||
content += f"**Vârsta:** {activity.age_group} | **Participanți:** {activity.participants} \n"
|
||||
content += f"**Durata:** {activity.duration} | **Materiale:** {activity.materials} \n\n"
|
||||
content += f"{activity.description}\n\n"
|
||||
content += f"**Fișier:** `{activity.file_path}`\n\n---\n\n"
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return output_path
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
"""Returnează statistici despre colecție"""
|
||||
total_activities = len(self.activities)
|
||||
|
||||
# Grupare pe categorii
|
||||
categories = {}
|
||||
age_groups = {}
|
||||
difficulties = {}
|
||||
|
||||
for activity in self.activities:
|
||||
categories[activity.category] = categories.get(activity.category, 0) + 1
|
||||
age_groups[activity.age_group] = age_groups.get(activity.age_group, 0) + 1
|
||||
difficulties[activity.difficulty] = difficulties.get(activity.difficulty, 0) + 1
|
||||
|
||||
return {
|
||||
'total_activities': total_activities,
|
||||
'categories': categories,
|
||||
'age_groups': age_groups,
|
||||
'difficulties': difficulties,
|
||||
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M')
|
||||
}
|
||||
|
||||
def main():
|
||||
"""Funcție principală pentru testarea sistemului"""
|
||||
print("🎮 GAME LIBRARY MANAGER - Inițializare...")
|
||||
|
||||
# Inițializare manager
|
||||
manager = GameLibraryManager()
|
||||
|
||||
print(f"✅ Încărcate {len(manager.activities)} activități")
|
||||
|
||||
# Exemplu căutări
|
||||
print("\n🔍 EXEMPLE DE CĂUTĂRI:")
|
||||
|
||||
# Căutare 1: Activități pentru copii mici
|
||||
print("\n1. Activități pentru copii 5-8 ani:")
|
||||
young_activities = manager.search_activities(age_min=5, keywords=["simplu"])
|
||||
for activity in young_activities[:3]: # Prima 3
|
||||
print(f" - {activity.title} ({activity.category})")
|
||||
|
||||
# Căutare 2: Team building
|
||||
print("\n2. Activități de team building:")
|
||||
team_activities = manager.search_activities(category="Team Building")
|
||||
for activity in team_activities:
|
||||
print(f" - {activity.title} ({activity.duration})")
|
||||
|
||||
# Căutare 3: Activități cu materiale minime
|
||||
print("\n3. Activități fără materiale:")
|
||||
no_materials = manager.search_activities(keywords=["fără materiale", "niciuna"])
|
||||
for activity in no_materials[:3]:
|
||||
print(f" - {activity.title} ({activity.materials})")
|
||||
|
||||
# Generare fișă exemplu
|
||||
print("\n📄 GENERARE FIȘĂ EXEMPLU:")
|
||||
if manager.activities:
|
||||
sample_activity = manager.activities[0]
|
||||
sheet = manager.generate_activity_sheet(sample_activity, "markdown")
|
||||
sheet_path = manager.index_path / f"FISA_EXEMPLU_{sample_activity.id}.md"
|
||||
with open(sheet_path, 'w', encoding='utf-8') as f:
|
||||
f.write(sheet)
|
||||
print(f" Fișă generată: {sheet_path}")
|
||||
|
||||
# Statistici
|
||||
print("\n📊 STATISTICI COLECȚIE:")
|
||||
stats = manager.get_statistics()
|
||||
print(f" Total activități: {stats['total_activities']}")
|
||||
print(f" Categorii: {list(stats['categories'].keys())}")
|
||||
print(f" Ultimul update: {stats['last_updated']}")
|
||||
|
||||
print("\n🎯 SISTEM INIȚIALIZAT CU SUCCES!")
|
||||
print("💡 Pentru utilizare interactivă, rulați: python -c \"from game_library_manager import GameLibraryManager; manager = GameLibraryManager(); print('Manager inițializat!')\"")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
579
src/indexer.py
Normal file
579
src/indexer.py
Normal file
@@ -0,0 +1,579 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MULTI-FORMAT INDEXER - Automated activity extraction from various file types
|
||||
|
||||
Author: Claude AI Assistant
|
||||
Date: 2025-09-09
|
||||
Purpose: Extract educational activities from PDF, DOC, HTML, MD, TXT files
|
||||
|
||||
Supported formats:
|
||||
- PDF: PyPDF2 + pdfplumber (backup)
|
||||
- DOC/DOCX: python-docx
|
||||
- HTML: BeautifulSoup4
|
||||
- MD: markdown
|
||||
- TXT: direct text processing
|
||||
|
||||
Requirements from PRD:
|
||||
- RF1: Extract activities from all supported formats
|
||||
- RF2: Auto-detect parameters (title, description, age, duration, materials)
|
||||
- RF3: Batch processing for existing files
|
||||
- RF4: Incremental processing for new files
|
||||
- RF5: Progress tracking
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
|
||||
# File processing imports
|
||||
try:
|
||||
import PyPDF2
|
||||
PDF_AVAILABLE = True
|
||||
except ImportError:
|
||||
PDF_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import pdfplumber
|
||||
PDFPLUMBER_AVAILABLE = True
|
||||
except ImportError:
|
||||
PDFPLUMBER_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from docx import Document as DocxDocument
|
||||
DOCX_AVAILABLE = True
|
||||
except ImportError:
|
||||
DOCX_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
HTML_AVAILABLE = True
|
||||
except ImportError:
|
||||
HTML_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import markdown
|
||||
MARKDOWN_AVAILABLE = True
|
||||
except ImportError:
|
||||
MARKDOWN_AVAILABLE = False
|
||||
|
||||
from database import DatabaseManager
|
||||
|
||||
class ActivityExtractor:
|
||||
"""Base class for activity extraction"""
|
||||
|
||||
# Pattern definitions for auto-detection
|
||||
TITLE_PATTERNS = [
|
||||
r'^#\s+(.+)$', # Markdown header
|
||||
r'^##\s+(.+)$', # Markdown subheader
|
||||
r'^\*\*([^*]+)\*\*', # Bold text
|
||||
r'^([A-Z][^.!?]*[.!?])$', # Capitalized sentence
|
||||
r'^(\d+[\.\)]\s*[A-Z][^.!?]*[.!?])$', # Numbered item
|
||||
]
|
||||
|
||||
AGE_PATTERNS = [
|
||||
r'(\d+)[-–](\d+)\s*ani', # "8-12 ani"
|
||||
r'(\d+)\+\s*ani', # "12+ ani"
|
||||
r'varsta\s*:?\s*(\d+)[-–](\d+)', # "Varsta: 8-12"
|
||||
r'age\s*:?\s*(\d+)[-–](\d+)', # "Age: 8-12"
|
||||
]
|
||||
|
||||
DURATION_PATTERNS = [
|
||||
r'(\d+)[-–](\d+)\s*min', # "15-30 min"
|
||||
r'(\d+)\s*minute', # "15 minute"
|
||||
r'durata\s*:?\s*(\d+)[-–](\d+)', # "Durata: 15-30"
|
||||
r'duration\s*:?\s*(\d+)[-–](\d+)', # "Duration: 15-30"
|
||||
]
|
||||
|
||||
PARTICIPANTS_PATTERNS = [
|
||||
r'(\d+)[-–](\d+)\s*(copii|persoane|participanti)', # "8-15 copii"
|
||||
r'(\d+)\+\s*(copii|persoane|participanti)', # "10+ copii"
|
||||
r'participanti\s*:?\s*(\d+)[-–](\d+)', # "Participanti: 5-10"
|
||||
r'players\s*:?\s*(\d+)[-–](\d+)', # "Players: 5-10"
|
||||
]
|
||||
|
||||
MATERIALS_PATTERNS = [
|
||||
r'materiale\s*:?\s*([^.\n]+)', # "Materiale: ..."
|
||||
r'materials\s*:?\s*([^.\n]+)', # "Materials: ..."
|
||||
r'echipament\s*:?\s*([^.\n]+)', # "Echipament: ..."
|
||||
r'equipment\s*:?\s*([^.\n]+)', # "Equipment: ..."
|
||||
r'fara\s+materiale', # "fara materiale"
|
||||
r'no\s+materials', # "no materials"
|
||||
]
|
||||
|
||||
def extract_parameter(self, text: str, patterns: List[str]) -> Optional[str]:
|
||||
"""Extract parameter using regex patterns"""
|
||||
text_lower = text.lower()
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text_lower, re.IGNORECASE | re.MULTILINE)
|
||||
if match:
|
||||
if len(match.groups()) == 1:
|
||||
return match.group(1).strip()
|
||||
elif len(match.groups()) == 2:
|
||||
return f"{match.group(1)}-{match.group(2)}"
|
||||
else:
|
||||
return match.group(0).strip()
|
||||
return None
|
||||
|
||||
def detect_activity_boundaries(self, text: str) -> List[Tuple[int, int]]:
|
||||
"""Detect where activities start and end in text"""
|
||||
# Simple heuristic: activities are separated by blank lines or headers
|
||||
lines = text.split('\n')
|
||||
boundaries = []
|
||||
start_idx = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if (line.strip() == '' and i > start_idx + 3) or \
|
||||
(re.match(r'^#{1,3}\s+', line) and i > start_idx):
|
||||
if i > start_idx:
|
||||
boundaries.append((start_idx, i))
|
||||
start_idx = i
|
||||
|
||||
# Add the last section
|
||||
if start_idx < len(lines) - 1:
|
||||
boundaries.append((start_idx, len(lines)))
|
||||
|
||||
return boundaries
|
||||
|
||||
def extract_activities_from_text(self, text: str, file_path: str, file_type: str) -> List[Dict]:
|
||||
"""Extract activities from plain text"""
|
||||
activities = []
|
||||
boundaries = self.detect_activity_boundaries(text)
|
||||
|
||||
for i, (start, end) in enumerate(boundaries):
|
||||
lines = text.split('\n')[start:end]
|
||||
section_text = '\n'.join(lines).strip()
|
||||
|
||||
if len(section_text) < 50: # Skip very short sections
|
||||
continue
|
||||
|
||||
# Extract title (first meaningful line)
|
||||
title = None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
title = line[:100] # Limit title length
|
||||
break
|
||||
|
||||
if not title:
|
||||
title = f"Activity {i+1}"
|
||||
|
||||
# Extract parameters
|
||||
age_group = self.extract_parameter(section_text, self.AGE_PATTERNS)
|
||||
duration = self.extract_parameter(section_text, self.DURATION_PATTERNS)
|
||||
participants = self.extract_parameter(section_text, self.PARTICIPANTS_PATTERNS)
|
||||
materials = self.extract_parameter(section_text, self.MATERIALS_PATTERNS)
|
||||
|
||||
# Create activity record
|
||||
activity = {
|
||||
'title': title,
|
||||
'description': section_text[:500], # First 500 chars as description
|
||||
'file_path': str(file_path),
|
||||
'file_type': file_type,
|
||||
'page_number': None,
|
||||
'tags': self._extract_keywords(section_text),
|
||||
'category': self._guess_category(section_text),
|
||||
'age_group': age_group or '',
|
||||
'participants': participants or '',
|
||||
'duration': duration or '',
|
||||
'materials': materials or '',
|
||||
'difficulty': 'mediu',
|
||||
'source_text': section_text
|
||||
}
|
||||
|
||||
activities.append(activity)
|
||||
|
||||
return activities
|
||||
|
||||
def _extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:
|
||||
"""Extract keywords from text"""
|
||||
# Simple keyword extraction based on common activity terms
|
||||
common_keywords = [
|
||||
'joc', 'game', 'echipa', 'team', 'copii', 'children', 'grupa', 'group',
|
||||
'activitate', 'activity', 'exercitiu', 'exercise', 'cooperare', 'cooperation',
|
||||
'creativitate', 'creativity', 'sport', 'concurs', 'competition', 'energie',
|
||||
'comunicare', 'communication', 'leadership', 'incredere', 'trust'
|
||||
]
|
||||
|
||||
text_lower = text.lower()
|
||||
found_keywords = []
|
||||
|
||||
for keyword in common_keywords:
|
||||
if keyword in text_lower and keyword not in found_keywords:
|
||||
found_keywords.append(keyword)
|
||||
if len(found_keywords) >= max_keywords:
|
||||
break
|
||||
|
||||
return found_keywords
|
||||
|
||||
def _guess_category(self, text: str) -> str:
|
||||
"""Guess activity category from text content"""
|
||||
text_lower = text.lower()
|
||||
|
||||
# Category mapping based on keywords
|
||||
categories = {
|
||||
'team building': ['team', 'echipa', 'cooperare', 'incredere', 'comunicare'],
|
||||
'jocuri cercetășești': ['scout', 'cercetasi', 'baden', 'uniform', 'patrol'],
|
||||
'activități educaționale': ['învățare', 'educativ', 'știință', 'biologie'],
|
||||
'orientare': ['busola', 'compass', 'hartă', 'orientare', 'azimut'],
|
||||
'primul ajutor': ['primul ajutor', 'first aid', 'medical', 'urgenta'],
|
||||
'escape room': ['puzzle', 'enigma', 'cod', 'mister', 'escape'],
|
||||
'camping & exterior': ['camping', 'natura', 'exterior', 'tabara', 'survival']
|
||||
}
|
||||
|
||||
for category, keywords in categories.items():
|
||||
if any(keyword in text_lower for keyword in keywords):
|
||||
return category
|
||||
|
||||
return 'diverse'
|
||||
|
||||
class PDFExtractor(ActivityExtractor):
|
||||
"""PDF file processor"""
|
||||
|
||||
def extract(self, file_path: Path) -> List[Dict]:
|
||||
"""Extract activities from PDF file"""
|
||||
activities = []
|
||||
|
||||
if not PDF_AVAILABLE and not PDFPLUMBER_AVAILABLE:
|
||||
raise ImportError("Neither PyPDF2 nor pdfplumber is available")
|
||||
|
||||
try:
|
||||
# Try pdfplumber first (better text extraction)
|
||||
if PDFPLUMBER_AVAILABLE:
|
||||
with pdfplumber.open(file_path) as pdf:
|
||||
full_text = ""
|
||||
for page in pdf.pages:
|
||||
text = page.extract_text()
|
||||
if text:
|
||||
full_text += text + "\n"
|
||||
|
||||
if full_text.strip():
|
||||
activities = self.extract_activities_from_text(
|
||||
full_text, file_path, 'pdf'
|
||||
)
|
||||
|
||||
# Fallback to PyPDF2
|
||||
elif PDF_AVAILABLE and not activities:
|
||||
with open(file_path, 'rb') as file:
|
||||
pdf_reader = PyPDF2.PdfReader(file)
|
||||
full_text = ""
|
||||
|
||||
for page in pdf_reader.pages:
|
||||
text = page.extract_text()
|
||||
if text:
|
||||
full_text += text + "\n"
|
||||
|
||||
if full_text.strip():
|
||||
activities = self.extract_activities_from_text(
|
||||
full_text, file_path, 'pdf'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing PDF {file_path}: {e}")
|
||||
return []
|
||||
|
||||
return activities
|
||||
|
||||
class DOCExtractor(ActivityExtractor):
|
||||
"""DOC/DOCX file processor"""
|
||||
|
||||
def extract(self, file_path: Path) -> List[Dict]:
|
||||
"""Extract activities from DOC/DOCX file"""
|
||||
if not DOCX_AVAILABLE:
|
||||
raise ImportError("python-docx not available")
|
||||
|
||||
try:
|
||||
doc = DocxDocument(file_path)
|
||||
full_text = ""
|
||||
|
||||
for paragraph in doc.paragraphs:
|
||||
if paragraph.text.strip():
|
||||
full_text += paragraph.text + "\n"
|
||||
|
||||
if full_text.strip():
|
||||
return self.extract_activities_from_text(full_text, file_path, 'docx')
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing DOCX {file_path}: {e}")
|
||||
|
||||
return []
|
||||
|
||||
class HTMLExtractor(ActivityExtractor):
|
||||
"""HTML file processor"""
|
||||
|
||||
def extract(self, file_path: Path) -> List[Dict]:
|
||||
"""Extract activities from HTML file"""
|
||||
if not HTML_AVAILABLE:
|
||||
raise ImportError("BeautifulSoup4 not available")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
|
||||
# Remove script and style elements
|
||||
for script in soup(["script", "style"]):
|
||||
script.decompose()
|
||||
|
||||
# Extract text
|
||||
text = soup.get_text()
|
||||
|
||||
# Clean up text
|
||||
lines = (line.strip() for line in text.splitlines())
|
||||
text = '\n'.join(line for line in lines if line)
|
||||
|
||||
if text.strip():
|
||||
return self.extract_activities_from_text(text, file_path, 'html')
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing HTML {file_path}: {e}")
|
||||
|
||||
return []
|
||||
|
||||
class MarkdownExtractor(ActivityExtractor):
|
||||
"""Markdown file processor"""
|
||||
|
||||
def extract(self, file_path: Path) -> List[Dict]:
|
||||
"""Extract activities from Markdown file"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
|
||||
# Convert to HTML first if markdown lib available, otherwise use raw text
|
||||
if MARKDOWN_AVAILABLE:
|
||||
html = markdown.markdown(content)
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
text = soup.get_text()
|
||||
else:
|
||||
text = content
|
||||
|
||||
if text.strip():
|
||||
return self.extract_activities_from_text(text, file_path, 'md')
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing Markdown {file_path}: {e}")
|
||||
|
||||
return []
|
||||
|
||||
class TextExtractor(ActivityExtractor):
|
||||
"""Plain text file processor"""
|
||||
|
||||
def extract(self, file_path: Path) -> List[Dict]:
|
||||
"""Extract activities from plain text file"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
text = file.read()
|
||||
|
||||
if text.strip():
|
||||
return self.extract_activities_from_text(text, file_path, 'txt')
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing text {file_path}: {e}")
|
||||
|
||||
return []
|
||||
|
||||
class MultiFormatIndexer:
|
||||
"""Main indexer class for processing multiple file formats"""
|
||||
|
||||
def __init__(self, base_path: str, db_path: str = "../data/activities.db"):
|
||||
self.base_path = Path(base_path)
|
||||
self.db = DatabaseManager(db_path)
|
||||
|
||||
# Initialize extractors
|
||||
self.extractors = {
|
||||
'.pdf': PDFExtractor(),
|
||||
'.doc': DOCExtractor(),
|
||||
'.docx': DOCExtractor(),
|
||||
'.html': HTMLExtractor(),
|
||||
'.htm': HTMLExtractor(),
|
||||
'.md': MarkdownExtractor(),
|
||||
'.txt': TextExtractor()
|
||||
}
|
||||
|
||||
self.processed_files = set()
|
||||
self.stats = {
|
||||
'total_files_found': 0,
|
||||
'total_files_processed': 0,
|
||||
'total_activities_extracted': 0,
|
||||
'files_failed': 0,
|
||||
'by_type': {}
|
||||
}
|
||||
|
||||
def get_supported_files(self) -> List[Path]:
|
||||
"""Get all supported files from base directory"""
|
||||
supported_files = []
|
||||
|
||||
for ext in self.extractors.keys():
|
||||
pattern = f"**/*{ext}"
|
||||
files = list(self.base_path.glob(pattern))
|
||||
supported_files.extend(files)
|
||||
|
||||
# Update stats
|
||||
if ext not in self.stats['by_type']:
|
||||
self.stats['by_type'][ext] = {'found': 0, 'processed': 0, 'activities': 0}
|
||||
self.stats['by_type'][ext]['found'] = len(files)
|
||||
|
||||
self.stats['total_files_found'] = len(supported_files)
|
||||
return supported_files
|
||||
|
||||
def process_file(self, file_path: Path) -> Tuple[int, str]:
|
||||
"""Process a single file and return (activities_count, status)"""
|
||||
file_ext = file_path.suffix.lower()
|
||||
|
||||
if file_ext not in self.extractors:
|
||||
return 0, f"Unsupported file type: {file_ext}"
|
||||
|
||||
extractor = self.extractors[file_ext]
|
||||
|
||||
try:
|
||||
print(f"📄 Processing: {file_path.name}")
|
||||
activities = extractor.extract(file_path)
|
||||
|
||||
# Insert activities into database
|
||||
inserted_count = 0
|
||||
for activity in activities:
|
||||
try:
|
||||
self.db.insert_activity(activity)
|
||||
inserted_count += 1
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Failed to insert activity: {e}")
|
||||
|
||||
# Log processing result
|
||||
self.db.log_file_processing(
|
||||
str(file_path), file_ext[1:], 'success', inserted_count
|
||||
)
|
||||
|
||||
# Update stats
|
||||
self.stats['total_files_processed'] += 1
|
||||
self.stats['total_activities_extracted'] += inserted_count
|
||||
self.stats['by_type'][file_ext]['processed'] += 1
|
||||
self.stats['by_type'][file_ext]['activities'] += inserted_count
|
||||
|
||||
print(f" ✅ Extracted {inserted_count} activities")
|
||||
return inserted_count, 'success'
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Processing failed: {e}"
|
||||
print(f" ❌ {error_msg}")
|
||||
|
||||
# Log error
|
||||
self.db.log_file_processing(str(file_path), file_ext[1:], 'error', 0, error_msg)
|
||||
|
||||
self.stats['files_failed'] += 1
|
||||
return 0, error_msg
|
||||
|
||||
def run_batch_indexing(self, clear_db: bool = False, max_files: int = None):
|
||||
"""Run batch indexing of all supported files"""
|
||||
print("🚀 MULTI-FORMAT INDEXER - Starting batch processing")
|
||||
print("=" * 60)
|
||||
|
||||
if clear_db:
|
||||
print("🗑️ Clearing existing database...")
|
||||
self.db.clear_database()
|
||||
|
||||
# Get files to process
|
||||
files_to_process = self.get_supported_files()
|
||||
|
||||
if max_files:
|
||||
files_to_process = files_to_process[:max_files]
|
||||
|
||||
print(f"📁 Found {len(files_to_process)} supported files")
|
||||
print(f"📊 File types: {list(self.stats['by_type'].keys())}")
|
||||
|
||||
# Check dependencies
|
||||
self._check_dependencies()
|
||||
|
||||
# Process files
|
||||
print("\n📄 PROCESSING FILES:")
|
||||
print("-" * 40)
|
||||
|
||||
start_time = datetime.now()
|
||||
|
||||
for i, file_path in enumerate(files_to_process, 1):
|
||||
print(f"\n[{i}/{len(files_to_process)}] ", end="")
|
||||
self.process_file(file_path)
|
||||
|
||||
end_time = datetime.now()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 INDEXING SUMMARY")
|
||||
print("=" * 60)
|
||||
print(f"⏱️ Total time: {duration:.2f} seconds")
|
||||
print(f"📁 Files found: {self.stats['total_files_found']}")
|
||||
print(f"✅ Files processed: {self.stats['total_files_processed']}")
|
||||
print(f"❌ Files failed: {self.stats['files_failed']}")
|
||||
print(f"🎮 Total activities extracted: {self.stats['total_activities_extracted']}")
|
||||
|
||||
print("\n📂 BY FILE TYPE:")
|
||||
for ext, stats in self.stats['by_type'].items():
|
||||
if stats['found'] > 0:
|
||||
success_rate = (stats['processed'] / stats['found']) * 100
|
||||
print(f" {ext}: {stats['processed']}/{stats['found']} files ({success_rate:.1f}%) → {stats['activities']} activities")
|
||||
|
||||
print(f"\n🎯 Average: {self.stats['total_activities_extracted'] / max(1, self.stats['total_files_processed']):.1f} activities per file")
|
||||
print("=" * 60)
|
||||
|
||||
def _check_dependencies(self):
|
||||
"""Check availability of required libraries"""
|
||||
print("\n🔧 CHECKING DEPENDENCIES:")
|
||||
deps = [
|
||||
("PyPDF2", PDF_AVAILABLE, "PDF processing"),
|
||||
("pdfplumber", PDFPLUMBER_AVAILABLE, "Enhanced PDF processing"),
|
||||
("python-docx", DOCX_AVAILABLE, "DOC/DOCX processing"),
|
||||
("BeautifulSoup4", HTML_AVAILABLE, "HTML processing"),
|
||||
("markdown", MARKDOWN_AVAILABLE, "Markdown processing")
|
||||
]
|
||||
|
||||
for name, available, purpose in deps:
|
||||
status = "✅" if available else "❌"
|
||||
print(f" {status} {name}: {purpose}")
|
||||
|
||||
# Check if we can process any files
|
||||
if not any([PDF_AVAILABLE, PDFPLUMBER_AVAILABLE, DOCX_AVAILABLE, HTML_AVAILABLE]):
|
||||
print("\n⚠️ WARNING: No file processing libraries available!")
|
||||
print(" Install with: pip install PyPDF2 python-docx beautifulsoup4 markdown pdfplumber")
|
||||
|
||||
def main():
|
||||
"""Command line interface"""
|
||||
parser = argparse.ArgumentParser(description="Multi-format activity indexer")
|
||||
|
||||
parser.add_argument('--base-path', '-p',
|
||||
default='/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri',
|
||||
help='Base directory to scan for files')
|
||||
|
||||
parser.add_argument('--db-path', '-d',
|
||||
default='../data/activities.db',
|
||||
help='Database file path')
|
||||
|
||||
parser.add_argument('--clear-db', '-c', action='store_true',
|
||||
help='Clear database before indexing')
|
||||
|
||||
parser.add_argument('--max-files', '-m', type=int,
|
||||
help='Maximum number of files to process (for testing)')
|
||||
|
||||
parser.add_argument('--test-mode', '-t', action='store_true',
|
||||
help='Run in test mode with limited files')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.test_mode:
|
||||
args.max_files = 5
|
||||
print("🧪 Running in TEST MODE (max 5 files)")
|
||||
|
||||
# Initialize and run indexer
|
||||
indexer = MultiFormatIndexer(args.base_path, args.db_path)
|
||||
indexer.run_batch_indexing(
|
||||
clear_db=args.clear_db,
|
||||
max_files=args.max_files
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
173
src/search_games.py
Normal file
173
src/search_games.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CĂUTARE INTERACTIVĂ JOCURI - Script simplu pentru căutări rapide
|
||||
|
||||
Folosire:
|
||||
python search_games.py
|
||||
sau
|
||||
python search_games.py --category "Team Building"
|
||||
python search_games.py --age 8 --duration 30
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from game_library_manager import GameLibraryManager
|
||||
|
||||
def interactive_search():
|
||||
"""Mod interactiv pentru căutări"""
|
||||
print("🎮 CĂUTARE INTERACTIVĂ ACTIVITĂȚI")
|
||||
print("=" * 50)
|
||||
|
||||
manager = GameLibraryManager()
|
||||
print(f"📚 Colecție încărcată: {len(manager.activities)} activități\n")
|
||||
|
||||
while True:
|
||||
print("\n🔍 CRITERII DE CĂUTARE (Enter pentru a sări):")
|
||||
|
||||
# Colectare criterii
|
||||
criteria = {}
|
||||
|
||||
category = input("Categoria (Team Building/Jocuri Cercetășești/etc.): ").strip()
|
||||
if category:
|
||||
criteria['category'] = category
|
||||
|
||||
age = input("Vârsta minimă (ex: 8): ").strip()
|
||||
if age and age.isdigit():
|
||||
criteria['age_min'] = int(age)
|
||||
|
||||
keywords_input = input("Cuvinte cheie (separate prin virgulă): ").strip()
|
||||
if keywords_input:
|
||||
criteria['keywords'] = [kw.strip() for kw in keywords_input.split(',')]
|
||||
|
||||
difficulty = input("Dificultatea (ușor/mediu/avansat): ").strip()
|
||||
if difficulty:
|
||||
criteria['difficulty'] = difficulty
|
||||
|
||||
# Căutare
|
||||
if criteria:
|
||||
results = manager.search_activities(**criteria)
|
||||
print(f"\n🎯 REZULTATE: {len(results)} activități găsite")
|
||||
print("-" * 50)
|
||||
|
||||
for i, activity in enumerate(results, 1):
|
||||
print(f"{i}. **{activity.title}**")
|
||||
print(f" 📂 {activity.category} → {activity.subcategory}")
|
||||
print(f" 👥 {activity.participants} | ⏰ {activity.duration} | 🎂 {activity.age_group}")
|
||||
print(f" 💡 {activity.description[:80]}...")
|
||||
print(f" 📁 {activity.file_path}")
|
||||
print()
|
||||
|
||||
# Opțiuni post-căutare
|
||||
if results:
|
||||
choice = input("\n📝 Generam fișe pentru activități? (da/nu/număr): ").strip().lower()
|
||||
|
||||
if choice == 'da':
|
||||
# Generează fișe pentru toate
|
||||
filename = f"cautare_rezultate_{len(results)}_activitati"
|
||||
export_path = manager.export_search_results(results, filename)
|
||||
print(f"✅ Exportat în: {export_path}")
|
||||
|
||||
elif choice.isdigit():
|
||||
# Generează fișă pentru activitate specifică
|
||||
idx = int(choice) - 1
|
||||
if 0 <= idx < len(results):
|
||||
activity = results[idx]
|
||||
sheet = manager.generate_activity_sheet(activity, "markdown")
|
||||
filename = f"FISA_{activity.id}_{activity.title.replace(' ', '_')}.md"
|
||||
sheet_path = manager.index_path / filename
|
||||
with open(sheet_path, 'w', encoding='utf-8') as f:
|
||||
f.write(sheet)
|
||||
print(f"✅ Fișă generată: {sheet_path}")
|
||||
else:
|
||||
print("❌ Nu ați specificat criterii de căutare")
|
||||
|
||||
# Continuă?
|
||||
continue_search = input("\n🔄 Altă căutare? (da/nu): ").strip().lower()
|
||||
if continue_search != 'da':
|
||||
break
|
||||
|
||||
print("\n👋 La revedere!")
|
||||
|
||||
def command_line_search(args):
|
||||
"""Căutare din linia de comandă"""
|
||||
manager = GameLibraryManager()
|
||||
|
||||
criteria = {}
|
||||
if args.category:
|
||||
criteria['category'] = args.category
|
||||
if args.age:
|
||||
criteria['age_min'] = args.age
|
||||
if args.keywords:
|
||||
criteria['keywords'] = args.keywords.split(',')
|
||||
if args.difficulty:
|
||||
criteria['difficulty'] = args.difficulty
|
||||
|
||||
results = manager.search_activities(**criteria)
|
||||
|
||||
print(f"🎯 Găsite {len(results)} activități:")
|
||||
for i, activity in enumerate(results, 1):
|
||||
print(f"{i}. {activity.title} ({activity.category})")
|
||||
print(f" {activity.age_group} | {activity.duration} | {activity.file_path}")
|
||||
|
||||
def show_categories():
|
||||
"""Afișează categoriile disponibile"""
|
||||
manager = GameLibraryManager()
|
||||
stats = manager.get_statistics()
|
||||
|
||||
print("📂 CATEGORII DISPONIBILE:")
|
||||
for category, count in stats['categories'].items():
|
||||
print(f" - {category} ({count} activități)")
|
||||
|
||||
def show_statistics():
|
||||
"""Afișează statistici complete"""
|
||||
manager = GameLibraryManager()
|
||||
stats = manager.get_statistics()
|
||||
|
||||
print("📊 STATISTICI COLECȚIE:")
|
||||
print(f" Total activități: {stats['total_activities']}")
|
||||
print(f" Ultimul update: {stats['last_updated']}")
|
||||
|
||||
print("\n📂 Distribuție pe categorii:")
|
||||
for category, count in stats['categories'].items():
|
||||
percentage = (count / stats['total_activities']) * 100
|
||||
print(f" - {category}: {count} ({percentage:.1f}%)")
|
||||
|
||||
print("\n🎂 Distribuție pe grupe de vârstă:")
|
||||
for age_group, count in stats['age_groups'].items():
|
||||
print(f" - {age_group}: {count}")
|
||||
|
||||
print("\n📊 Distribuție pe dificultate:")
|
||||
for difficulty, count in stats['difficulties'].items():
|
||||
print(f" - {difficulty}: {count}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Căutare în colecția de jocuri și activități")
|
||||
|
||||
# Opțiuni de căutare
|
||||
parser.add_argument('--category', '-c', help='Categoria activității')
|
||||
parser.add_argument('--age', '-a', type=int, help='Vârsta minimă')
|
||||
parser.add_argument('--keywords', '-k', help='Cuvinte cheie (separate prin virgulă)')
|
||||
parser.add_argument('--difficulty', '-d', help='Nivelul de dificultate')
|
||||
|
||||
# Opțiuni informaționale
|
||||
parser.add_argument('--categories', action='store_true', help='Afișează categoriile disponibile')
|
||||
parser.add_argument('--stats', action='store_true', help='Afișează statistici complete')
|
||||
parser.add_argument('--interactive', '-i', action='store_true', help='Mod interactiv')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Verifică dacă nu sunt argumente - pornește modul interactiv
|
||||
if len(sys.argv) == 1:
|
||||
interactive_search()
|
||||
elif args.categories:
|
||||
show_categories()
|
||||
elif args.stats:
|
||||
show_statistics()
|
||||
elif args.interactive:
|
||||
interactive_search()
|
||||
else:
|
||||
command_line_search(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user