"""
Advanced Search and Filters Manager
This module provides advanced search functionality, filtering capabilities,
and search result management for the AniWorld web interface.
"""
from typing import Dict, List, Any, Optional, Set
import re
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify
import json
class AdvancedSearchManager:
"""Manages advanced search and filtering functionality."""
def __init__(self, app=None):
self.app = app
self.search_history = []
self.saved_searches = {}
self.filter_presets = {
'recent': {
'name': 'Recently Added',
'filters': {
'date_added': {'operator': 'last_days', 'value': 7}
}
},
'downloading': {
'name': 'Currently Downloading',
'filters': {
'status': {'operator': 'equals', 'value': 'downloading'}
}
},
'completed': {
'name': 'Completed Series',
'filters': {
'status': {'operator': 'equals', 'value': 'completed'}
}
},
'high_rated': {
'name': 'Highly Rated',
'filters': {
'rating': {'operator': 'greater_than', 'value': 8.0}
}
}
}
def init_app(self, app):
"""Initialize with Flask app."""
self.app = app
def get_search_js(self):
"""Generate JavaScript code for advanced search functionality."""
return f"""
// AniWorld Advanced Search Manager
class AdvancedSearchManager {{
constructor() {{
this.currentFilters = {{}};
this.searchHistory = [];
this.savedSearches = {{}};
this.filterPresets = {json.dumps(self.filter_presets)};
this.searchResults = [];
this.sortBy = 'name';
this.sortOrder = 'asc';
this.init();
}}
init() {{
this.createSearchInterface();
this.setupSearchEvents();
this.loadSearchHistory();
this.loadSavedSearches();
this.setupKeyboardShortcuts();
}}
createSearchInterface() {{
this.createSearchBar();
this.createAdvancedFilters();
this.createSearchResults();
this.createQuickFilters();
}}
createSearchBar() {{
const existingSearch = document.querySelector('.advanced-search-bar');
if (existingSearch) return;
const searchContainer = document.createElement('div');
searchContainer.className = 'advanced-search-bar mb-4';
searchContainer.innerHTML = `
`;
// Insert at the top of main content
const mainContent = document.querySelector('.main-content, .container-fluid');
if (mainContent) {{
mainContent.insertBefore(searchContainer, mainContent.firstChild);
}}
}}
createAdvancedFilters() {{
const filtersModal = document.createElement('div');
filtersModal.id = 'advanced-filters-modal';
filtersModal.className = 'modal fade';
filtersModal.innerHTML = `
Content Filters
Status & Progress
`;
document.body.appendChild(filtersModal);
// Populate filter presets
this.populateFilterPresets();
}}
createSearchResults() {{
const existingResults = document.querySelector('.search-results-container');
if (existingResults) return;
const resultsContainer = document.createElement('div');
resultsContainer.className = 'search-results-container';
resultsContainer.innerHTML = `
0 results
`;
const mainContent = document.querySelector('.main-content, .container-fluid');
if (mainContent) {{
mainContent.appendChild(resultsContainer);
}}
}}
createQuickFilters() {{
const quickFiltersContainer = document.createElement('div');
quickFiltersContainer.className = 'quick-filters mb-3';
quickFiltersContainer.innerHTML = `
Quick Filters:
`;
const searchBar = document.querySelector('.advanced-search-bar');
if (searchBar) {{
searchBar.parentNode.insertBefore(quickFiltersContainer, searchBar.nextSibling);
}}
}}
setupSearchEvents() {{
// Search input events
const searchInput = document.getElementById('search-input');
if (searchInput) {{
searchInput.addEventListener('input', this.handleSearchInput.bind(this));
searchInput.addEventListener('keypress', (e) => {{
if (e.key === 'Enter') {{
this.performSearch();
}}
}});
}}
// Search button
document.getElementById('search-btn')?.addEventListener('click', this.performSearch.bind(this));
// Advanced filters button
document.getElementById('advanced-filters-btn')?.addEventListener('click', this.showAdvancedFilters.bind(this));
// Clear search button
document.getElementById('clear-search-btn')?.addEventListener('click', this.clearSearch.bind(this));
// Save search button
document.getElementById('save-search-btn')?.addEventListener('click', this.saveCurrentSearch.bind(this));
// Sort controls
document.getElementById('sort-by')?.addEventListener('change', this.handleSortChange.bind(this));
document.getElementById('sort-order-btn')?.addEventListener('click', this.toggleSortOrder.bind(this));
// Quick filter tags
document.addEventListener('click', (e) => {{
if (e.target.classList.contains('filter-tag')) {{
this.applyFilterPreset(e.target.dataset.preset);
}}
}});
// Filter date change
document.getElementById('filter-date-added')?.addEventListener('change', (e) => {{
const customRange = document.getElementById('custom-date-range');
if (e.target.value === 'custom') {{
customRange.classList.remove('d-none');
}} else {{
customRange.classList.add('d-none');
}}
}});
// Apply filters button
document.getElementById('apply-filters-btn')?.addEventListener('click', this.applyAdvancedFilters.bind(this));
// Clear filters button
document.getElementById('clear-filters-btn')?.addEventListener('click', this.clearAllFilters.bind(this));
}}
setupKeyboardShortcuts() {{
document.addEventListener('keydown', (e) => {{
if (e.ctrlKey || e.metaKey) {{
switch(e.key) {{
case 'f':
e.preventDefault();
document.getElementById('search-input')?.focus();
break;
case 'k':
e.preventDefault();
this.showAdvancedFilters();
break;
}}
}}
}});
}}
handleSearchInput(e) {{
const query = e.target.value;
// Show suggestions after 2 characters
if (query.length >= 2) {{
this.showSearchSuggestions(query);
}} else {{
this.hideSearchSuggestions();
}}
}}
showSearchSuggestions(query) {{
// Implement search suggestions
// This would call an API to get suggestions
fetch(`/api/search/suggestions?q=${{encodeURIComponent(query)}}`)
.then(response => response.json())
.then(data => {{
this.displaySuggestions(data.suggestions);
}})
.catch(error => console.error('Error fetching suggestions:', error));
}}
displaySuggestions(suggestions) {{
// Display search suggestions dropdown
let suggestionsDropdown = document.getElementById('search-suggestions');
if (!suggestionsDropdown) {{
suggestionsDropdown = document.createElement('div');
suggestionsDropdown.id = 'search-suggestions';
suggestionsDropdown.className = 'search-suggestions dropdown-menu show';
const searchInput = document.getElementById('search-input');
searchInput.parentNode.appendChild(suggestionsDropdown);
}}
suggestionsDropdown.innerHTML = suggestions.map(suggestion => `
${{suggestion.text}}
${{suggestion.type}}
`).join('');
// Add click handlers
suggestionsDropdown.querySelectorAll('.suggestion-item').forEach(item => {{
item.addEventListener('click', (e) => {{
e.preventDefault();
document.getElementById('search-input').value = item.dataset.value;
this.performSearch();
this.hideSearchSuggestions();
}});
}});
}}
hideSearchSuggestions() {{
const suggestionsDropdown = document.getElementById('search-suggestions');
if (suggestionsDropdown) {{
suggestionsDropdown.remove();
}}
}}
async performSearch() {{
const query = document.getElementById('search-input').value;
const searchType = document.getElementById('search-type').value;
if (!query.trim()) {{
this.clearResults();
return;
}}
// Add to search history
this.addToSearchHistory(query);
// Show loading
this.showSearchLoading();
try {{
const searchParams = {{
query: query,
type: searchType,
filters: this.currentFilters,
sort_by: this.sortBy,
sort_order: this.sortOrder
}};
const response = await fetch('/api/search', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json'
}},
body: JSON.stringify(searchParams)
}});
const data = await response.json();
if (data.success) {{
this.searchResults = data.results;
this.displaySearchResults(data);
this.updateSearchInfo(query, data.total_results);
}} else {{
this.showSearchError(data.error);
}}
}} catch (error) {{
console.error('Search error:', error);
this.showSearchError('Search failed. Please try again.');
}}
}}
showSearchLoading() {{
const resultsContainer = document.getElementById('search-results');
if (resultsContainer) {{
resultsContainer.innerHTML = `
Searching...
Searching...
`;
}}
}}
displaySearchResults(data) {{
const resultsContainer = document.getElementById('search-results');
if (!resultsContainer) return;
if (data.results.length === 0) {{
resultsContainer.innerHTML = `
No results found. Try adjusting your search terms or filters.
`;
return;
}}
const resultsHTML = data.results.map(item => `
${{item.name}}
${{item.genre || 'Unknown'}}
${{item.year || 'N/A'}}
${{item.episodes || 0}} episodes
${{item.rating ? ` ${{item.rating}}` : ''}}
`).join('');
resultsContainer.innerHTML = resultsHTML;
// Update pagination if needed
this.updatePagination(data);
}}
updateSearchInfo(query, totalResults) {{
const resultsCount = document.querySelector('.results-count');
const searchQuery = document.querySelector('.search-query');
if (resultsCount) {{
resultsCount.textContent = `${{totalResults}} result${{totalResults === 1 ? '' : 's'}}`;
}}
if (searchQuery) {{
searchQuery.textContent = query ? `for "${{query}}"` : '';
}}
}}
showSearchError(error) {{
const resultsContainer = document.getElementById('search-results');
if (resultsContainer) {{
resultsContainer.innerHTML = `
${{error}}
`;
}}
}}
clearResults() {{
const resultsContainer = document.getElementById('search-results');
if (resultsContainer) {{
resultsContainer.innerHTML = '';
}}
this.updateSearchInfo('', 0);
}}
clearSearch() {{
document.getElementById('search-input').value = '';
document.getElementById('search-type').value = 'all';
this.currentFilters = {{}};
this.clearResults();
this.updateActiveFilters();
this.hideSearchSuggestions();
}}
applyFilterPreset(presetName) {{
const preset = this.filterPresets[presetName];
if (preset) {{
this.currentFilters = {{ ...preset.filters }};
this.updateActiveFilters();
this.performSearch();
}}
}}
showAdvancedFilters() {{
const modal = document.getElementById('advanced-filters-modal');
if (modal) {{
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}}
}}
applyAdvancedFilters() {{
// Collect filter values from the modal
const filters = {{}};
// Genre filter
const genre = document.getElementById('filter-genre');
if (genre.selectedOptions.length > 0) {{
filters.genre = Array.from(genre.selectedOptions).map(o => o.value);
}}
// Year range
const yearFrom = document.getElementById('filter-year-from').value;
const yearTo = document.getElementById('filter-year-to').value;
if (yearFrom || yearTo) {{
filters.year_range = {{ from: yearFrom, to: yearTo }};
}}
// Rating range
const ratingMin = document.getElementById('filter-rating-min').value;
const ratingMax = document.getElementById('filter-rating-max').value;
if (ratingMin || ratingMax) {{
filters.rating_range = {{ min: ratingMin, max: ratingMax }};
}}
// Status
const status = document.getElementById('filter-status');
if (status.selectedOptions.length > 0) {{
filters.status = Array.from(status.selectedOptions).map(o => o.value);
}}
// Episode count range
const episodesMin = document.getElementById('filter-episodes-min').value;
const episodesMax = document.getElementById('filter-episodes-max').value;
if (episodesMin || episodesMax) {{
filters.episodes_range = {{ min: episodesMin, max: episodesMax }};
}}
// Date added
const dateAdded = document.getElementById('filter-date-added').value;
if (dateAdded) {{
if (dateAdded === 'custom') {{
const dateFrom = document.getElementById('filter-date-from').value;
const dateTo = document.getElementById('filter-date-to').value;
if (dateFrom || dateTo) {{
filters.date_range = {{ from: dateFrom, to: dateTo }};
}}
}} else {{
filters.date_added = dateAdded;
}}
}}
this.currentFilters = filters;
this.updateActiveFilters();
// Close modal and perform search
const modal = bootstrap.Modal.getInstance(document.getElementById('advanced-filters-modal'));
modal.hide();
this.performSearch();
}}
clearAllFilters() {{
this.currentFilters = {{}};
// Clear form fields
document.getElementById('filter-genre').selectedIndex = -1;
document.getElementById('filter-year-from').value = '';
document.getElementById('filter-year-to').value = '';
document.getElementById('filter-rating-min').value = '';
document.getElementById('filter-rating-max').value = '';
document.getElementById('filter-status').selectedIndex = -1;
document.getElementById('filter-episodes-min').value = '';
document.getElementById('filter-episodes-max').value = '';
document.getElementById('filter-date-added').value = '';
document.getElementById('filter-date-from').value = '';
document.getElementById('filter-date-to').value = '';
this.updateActiveFilters();
this.performSearch();
}}
updateActiveFilters() {{
const activeFiltersContainer = document.getElementById('active-filters');
if (!activeFiltersContainer) return;
const filterTags = [];
for (const [key, value] of Object.entries(this.currentFilters)) {{
let filterText = '';
switch(key) {{
case 'genre':
filterText = `Genre: ${{value.join(', ')}}`;
break;
case 'year_range':
filterText = `Year: ${{value.from || '?'}} - ${{value.to || '?'}}`;
break;
case 'rating_range':
filterText = `Rating: ${{value.min || '?'}} - ${{value.max || '?'}}`;
break;
case 'status':
filterText = `Status: ${{value.join(', ')}}`;
break;
case 'episodes_range':
filterText = `Episodes: ${{value.min || '?'}} - ${{value.max || '?'}}`;
break;
case 'date_added':
filterText = `Added: ${{value}}`;
break;
case 'date_range':
filterText = `Date: ${{value.from || '?'}} - ${{value.to || '?'}}`;
break;
}}
if (filterText) {{
filterTags.push(`
${{filterText}}
`);
}}
}}
if (filterTags.length > 0) {{
activeFiltersContainer.innerHTML = `
Active filters:
${{filterTags.join('')}}
`;
}} else {{
activeFiltersContainer.innerHTML = '';
}}
}}
removeFilter(key) {{
delete this.currentFilters[key];
this.updateActiveFilters();
this.performSearch();
}}
handleSortChange(e) {{
this.sortBy = e.target.value;
this.performSearch();
}}
toggleSortOrder() {{
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
const btn = document.getElementById('sort-order-btn');
const icon = btn.querySelector('i');
if (this.sortOrder === 'desc') {{
icon.className = 'fas fa-sort-alpha-up';
}} else {{
icon.className = 'fas fa-sort-alpha-down';
}}
this.performSearch();
}}
addToSearchHistory(query) {{
// Remove if already exists
this.searchHistory = this.searchHistory.filter(item => item.query !== query);
// Add to beginning
this.searchHistory.unshift({{
query: query,
timestamp: Date.now(),
filters: {{ ...this.currentFilters }}
}});
// Keep only last 10
if (this.searchHistory.length > 10) {{
this.searchHistory = this.searchHistory.slice(0, 10);
}}
this.saveSearchHistory();
this.updateSearchHistoryMenu();
}}
loadSearchHistory() {{
const stored = localStorage.getItem('aniworld_search_history');
if (stored) {{
try {{
this.searchHistory = JSON.parse(stored);
}} catch (error) {{
console.error('Error loading search history:', error);
this.searchHistory = [];
}}
}}
this.updateSearchHistoryMenu();
}}
saveSearchHistory() {{
localStorage.setItem('aniworld_search_history', JSON.stringify(this.searchHistory));
}}
updateSearchHistoryMenu() {{
const menu = document.getElementById('search-history-menu');
if (!menu) return;
if (this.searchHistory.length === 0) {{
menu.innerHTML = 'No recent searches';
return;
}}
const historyItems = this.searchHistory.map(item => `
${{item.query}}
${{this.formatTimestamp(item.timestamp)}}
`).join('');
menu.innerHTML = `
${{historyItems}}
Clear History
`;
// Add click handlers
menu.querySelectorAll('.history-item').forEach(item => {{
item.addEventListener('click', (e) => {{
e.preventDefault();
document.getElementById('search-input').value = item.dataset.query;
this.currentFilters = JSON.parse(item.dataset.filters);
this.updateActiveFilters();
this.performSearch();
}});
}});
}}
clearSearchHistory() {{
this.searchHistory = [];
this.saveSearchHistory();
this.updateSearchHistoryMenu();
}}
saveCurrentSearch() {{
const query = document.getElementById('search-input').value;
if (!query.trim()) return;
const name = prompt('Enter a name for this search:');
if (!name) return;
const searchId = Date.now().toString();
this.savedSearches[searchId] = {{
name: name,
query: query,
filters: {{ ...this.currentFilters }},
created: Date.now()
}};
this.saveSavedSearches();
this.populateSavedSearches();
this.showToast('Search saved successfully', 'success');
}}
loadSavedSearches() {{
const stored = localStorage.getItem('aniworld_saved_searches');
if (stored) {{
try {{
this.savedSearches = JSON.parse(stored);
}} catch (error) {{
console.error('Error loading saved searches:', error);
this.savedSearches = {{}};
}}
}}
}}
saveSavedSearches() {{
localStorage.setItem('aniworld_saved_searches', JSON.stringify(this.savedSearches));
}}
populateSavedSearches() {{
const container = document.getElementById('saved-searches-list');
if (!container) return;
if (Object.keys(this.savedSearches).length === 0) {{
container.innerHTML = 'No saved searches
';
return;
}}
const searches = Object.entries(this.savedSearches).map(([id, search]) => `
${{search.name}}
${{search.query}}
${{this.formatTimestamp(search.created)}}
`).join('');
container.innerHTML = searches;
}}
loadSavedSearch(searchId) {{
const search = this.savedSearches[searchId];
if (!search) return;
document.getElementById('search-input').value = search.query;
this.currentFilters = {{ ...search.filters }};
this.updateActiveFilters();
this.performSearch();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('advanced-filters-modal'));
if (modal) {{
modal.hide();
}}
}}
deleteSavedSearch(searchId) {{
if (confirm('Are you sure you want to delete this saved search?')) {{
delete this.savedSearches[searchId];
this.saveSavedSearches();
this.populateSavedSearches();
}}
}}
populateFilterPresets() {{
const container = document.getElementById('filter-presets');
if (!container) return;
const presets = Object.entries(this.filterPresets).map(([key, preset]) => `
`).join('');
container.innerHTML = presets;
}}
formatTimestamp(timestamp) {{
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${{Math.floor(diff / 60000)}}m ago`;
if (diff < 86400000) return `${{Math.floor(diff / 3600000)}}h ago`;
return date.toLocaleDateString();
}}
showToast(message, type = 'info') {{
// Create and show a toast notification
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${{type === 'error' ? 'danger' : type}}`;
toast.innerHTML = `
`;
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {{
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
document.body.appendChild(toastContainer);
}}
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {{
if (toast.parentNode) {{
toastContainer.removeChild(toast);
}}
}});
}}
}}
// Initialize advanced search when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {{
window.advancedSearch = new AdvancedSearchManager();
}});
"""
def get_css(self):
"""Generate CSS for advanced search functionality."""
return """
/* Advanced Search Styles */
.advanced-search-bar {
background: var(--bs-light);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--bs-border-color);
}
.search-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
max-height: 300px;
overflow-y: auto;
}
.suggestion-item {
display: flex;
justify-content: between;
align-items: center;
}
.suggestion-item i {
margin-right: 0.5rem;
width: 16px;
}
.quick-filters .filter-tag.active {
background-color: var(--bs-primary);
color: white;
}
.active-filters .badge {
display: inline-flex;
align-items: center;
}
.active-filters .btn-close {
--bs-btn-close-bg: none;
}
.search-result-item {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.search-result-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.search-info .results-count {
font-weight: 600;
color: var(--bs-primary);
}
.sort-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
#advanced-filters-modal .modal-dialog {
max-width: 800px;
}
#advanced-filters-modal h6 {
color: var(--bs-primary);
border-bottom: 1px solid var(--bs-border-color);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
}
.saved-search-item {
background: var(--bs-light);
}
.saved-search-item:hover {
background: var(--bs-secondary-bg);
}
/* Search loading animation */
.search-results .spinner-border {
width: 3rem;
height: 3rem;
}
/* Responsive design */
@media (max-width: 768px) {
.advanced-search-bar .row {
gap: 0.5rem;
}
.advanced-search-bar .col-md-3,
.advanced-search-bar .col-md-6 {
flex: 1 1 100%;
max-width: 100%;
}
.quick-filters {
flex-direction: column;
align-items: flex-start;
}
.quick-filters > div {
flex-wrap: wrap;
}
.search-result-item .row {
flex-direction: column;
}
.search-result-item .text-end {
text-align: start !important;
margin-top: 0.5rem;
}
.sort-controls {
justify-content: space-between;
width: 100%;
}
}
/* Dark theme support */
[data-bs-theme="dark"] .advanced-search-bar {
background: var(--bs-dark);
border-color: var(--bs-border-color-translucent);
}
[data-bs-theme="dark"] .search-suggestions {
background: var(--bs-dark);
border-color: var(--bs-border-color-translucent);
}
[data-bs-theme="dark"] .saved-search-item {
background: var(--bs-dark);
}
/* Animation for search results */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.search-result-item {
animation: fadeInUp 0.3s ease-out;
}
/* Accessibility improvements */
.search-suggestions .dropdown-item:focus {
background-color: var(--bs-primary);
color: var(--bs-white);
}
@media (prefers-reduced-motion: reduce) {
.search-result-item,
.search-result-item:hover {
transition: none;
transform: none;
}
.search-result-item {
animation: none;
}
}
"""
def search_series(self, query: str, search_type: str = 'all', filters: Dict[str, Any] = None,
sort_by: str = 'name', sort_order: str = 'asc') -> Dict[str, Any]:
"""Search for series based on query and filters."""
try:
# This would implement actual search logic
# For now, return mock data
results = [
{
'id': '1',
'name': 'Attack on Titan',
'genre': 'Action',
'year': 2013,
'episodes': 75,
'rating': 9.0,
'status': 'completed'
},
{
'id': '2',
'name': 'Death Note',
'genre': 'Thriller',
'year': 2006,
'episodes': 37,
'rating': 9.1,
'status': 'completed'
}
]
# Apply search query filtering
if query:
results = [r for r in results if query.lower() in r['name'].lower()]
# Apply filters if provided
if filters:
if 'genre' in filters:
genres = filters['genre'] if isinstance(filters['genre'], list) else [filters['genre']]
results = [r for r in results if r.get('genre') in genres]
if 'status' in filters:
statuses = filters['status'] if isinstance(filters['status'], list) else [filters['status']]
results = [r for r in results if r.get('status') in statuses]
if 'year_range' in filters:
year_range = filters['year_range']
if year_range.get('from'):
results = [r for r in results if r.get('year', 0) >= int(year_range['from'])]
if year_range.get('to'):
results = [r for r in results if r.get('year', 0) <= int(year_range['to'])]
# Apply sorting
if sort_by in ['name', 'year', 'rating', 'episodes']:
reverse = (sort_order == 'desc')
results.sort(key=lambda x: x.get(sort_by, ''), reverse=reverse)
return {
'success': True,
'results': results,
'total_results': len(results),
'query': query,
'filters': filters or {}
}
except Exception as e:
return {
'success': False,
'error': str(e),
'results': [],
'total_results': 0
}
def get_search_suggestions(self, query: str) -> List[Dict[str, Any]]:
"""Get search suggestions for a query."""
suggestions = []
# Mock suggestions - replace with actual implementation
if 'attack' in query.lower():
suggestions.append({
'text': 'Attack on Titan',
'value': 'Attack on Titan',
'type': 'Series',
'icon': 'fa-film'
})
if 'action' in query.lower():
suggestions.append({
'text': 'Action',
'value': 'Action',
'type': 'Genre',
'icon': 'fa-tags'
})
return suggestions
# Create search API blueprint
search_bp = Blueprint('search', __name__, url_prefix='/api')
# Global search manager instance
search_manager = AdvancedSearchManager()
@search_bp.route('/search', methods=['POST'])
def search():
"""Perform search with query and filters."""
try:
data = request.get_json()
query = data.get('query', '')
search_type = data.get('type', 'all')
filters = data.get('filters', {})
sort_by = data.get('sort_by', 'name')
sort_order = data.get('sort_order', 'asc')
result = search_manager.search_series(query, search_type, filters, sort_by, sort_order)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'error': str(e),
'results': [],
'total_results': 0
}), 500
@search_bp.route('/search/suggestions', methods=['GET'])
def get_suggestions():
"""Get search suggestions."""
try:
query = request.args.get('q', '')
suggestions = search_manager.get_search_suggestions(query)
return jsonify({
'success': True,
'suggestions': suggestions
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e),
'suggestions': []
}), 500
# Export the search manager
advanced_search_manager = AdvancedSearchManager()