Spaces:
Running
Running
| from flask import Flask, render_template, request, jsonify | |
| import requests | |
| import os | |
| import json | |
| import time | |
| app = Flask(__name__) | |
| # Function to fetch trending spaces from Huggingface | |
| def fetch_trending_spaces(limit=100): | |
| try: | |
| # ํธ๋ ๋ฉ ์คํ์ด์ค ๊ฐ์ ธ์ค๊ธฐ (์๋์ API ํ๋ผ๋ฏธํฐ ์ฌ์ฉ) | |
| url = "https://huggingface.co/api/spaces" | |
| params = { | |
| "limit": limit, | |
| "sort": "trending" # ๋จ์ํ trending ํ๋ผ๋ฏธํฐ๋ง ์ฌ์ฉ | |
| } | |
| response = requests.get(url, params=params, timeout=10) | |
| if response.status_code == 200: | |
| # None ๊ฐ์ด ์๋ ํญ๋ชฉ ํํฐ๋ง | |
| spaces = response.json() | |
| filtered_spaces = [space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None'] | |
| return filtered_spaces | |
| else: | |
| print(f"Error fetching trending spaces: {response.status_code}") | |
| return [] | |
| except Exception as e: | |
| print(f"Exception when fetching trending spaces: {e}") | |
| return [] | |
| # Transform Huggingface URL to direct space URL | |
| def transform_url(owner, name): | |
| return f"https://{owner}-{name}.hf.space" | |
| # Get space details | |
| def get_space_details(space_data): | |
| try: | |
| # ๊ณตํต ์ ๋ณด ์ถ์ถ | |
| if '/' in space_data.get('id', ''): | |
| owner, name = space_data.get('id', '').split('/', 1) | |
| else: | |
| owner = space_data.get('owner', '') | |
| name = space_data.get('id', '') | |
| # None์ด ํฌํจ๋ ๊ฒฝ์ฐ ๋ฌด์ | |
| if owner == 'None' or name == 'None': | |
| return None | |
| # URL ๊ตฌ์ฑ | |
| original_url = f"https://huggingface.co/spaces/{owner}/{name}" | |
| embed_url = transform_url(owner, name) | |
| # ์ข์์ ์ | |
| likes_count = space_data.get('likes', 0) | |
| # ์ ๋ชฉ ์ถ์ถ | |
| title = space_data.get('title', name) | |
| # ํ๊ทธ | |
| tags = space_data.get('tags', []) | |
| return { | |
| 'url': original_url, | |
| 'embedUrl': embed_url, | |
| 'title': title, | |
| 'owner': owner, | |
| 'likes_count': likes_count, | |
| 'tags': tags | |
| } | |
| except Exception as e: | |
| print(f"Error processing space data: {e}") | |
| return None | |
| # Homepage route | |
| def home(): | |
| return render_template('index.html') | |
| # Trending spaces API | |
| def trending_spaces(): | |
| search_query = request.args.get('search', '').lower() | |
| limit = int(request.args.get('limit', 100)) | |
| # Fetch trending spaces | |
| spaces_data = fetch_trending_spaces(limit) | |
| # Process and filter spaces | |
| results = [] | |
| for index, space_data in enumerate(spaces_data): | |
| space_info = get_space_details(space_data) | |
| if not space_info: | |
| continue | |
| # Apply search filter if needed | |
| if search_query: | |
| title = space_info['title'].lower() | |
| owner = space_info['owner'].lower() | |
| url = space_info['url'].lower() | |
| tags = ' '.join([str(tag) for tag in space_info.get('tags', [])]).lower() | |
| if (search_query not in title and | |
| search_query not in owner and | |
| search_query not in url and | |
| search_query not in tags): | |
| continue | |
| results.append(space_info) | |
| return jsonify(results) | |
| if __name__ == '__main__': | |
| # Create templates folder | |
| os.makedirs('templates', exist_ok=True) | |
| # Create index.html file | |
| with open('templates/index.html', 'w', encoding='utf-8') as f: | |
| f.write(''' | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>์ธ๊ธฐ ํ๊น ํ์ด์ค ์คํ์ด์ค</title> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| line-height: 1.6; | |
| margin: 0; | |
| padding: 0; | |
| color: #333; | |
| background-color: #f5f8fa; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 1rem; | |
| } | |
| .header { | |
| background-color: #ffffff; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| text-align: center; | |
| } | |
| .header h1 { | |
| margin: 0; | |
| color: #2c3e50; | |
| font-size: 1.8rem; | |
| } | |
| .filter-controls { | |
| background-color: #ffffff; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| input[type="text"] { | |
| padding: 0.7rem; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| font-size: 1rem; | |
| width: 300px; | |
| } | |
| button.refresh-btn { | |
| padding: 0.7rem 1.2rem; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| transition: background-color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| button.refresh-btn:hover { | |
| background-color: #45a049; | |
| } | |
| .refresh-icon { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid white; | |
| border-top-color: transparent; | |
| border-radius: 50%; | |
| animation: none; | |
| } | |
| .refreshing .refresh-icon { | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .grid-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| .grid-item { | |
| background-color: #ffffff; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| display: flex; | |
| flex-direction: column; | |
| height: 600px; | |
| position: relative; | |
| } | |
| .grid-header { | |
| padding: 0.8rem; | |
| border-bottom: 1px solid #eee; | |
| background-color: #f9f9f9; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| z-index: 2; | |
| } | |
| .grid-header-left { | |
| display: flex; | |
| flex-direction: column; | |
| max-width: 70%; | |
| } | |
| .grid-header h3 { | |
| margin: 0; | |
| padding: 0; | |
| font-size: 1.1rem; | |
| color: #333; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .owner-info { | |
| font-size: 0.85rem; | |
| color: #666; | |
| margin-top: 3px; | |
| } | |
| .grid-actions { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .open-link { | |
| color: #4CAF50; | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| margin-left: 10px; | |
| } | |
| .likes-counter { | |
| display: flex; | |
| align-items: center; | |
| font-size: 0.9rem; | |
| color: #e91e63; | |
| } | |
| .likes-counter span { | |
| margin-left: 4px; | |
| } | |
| .grid-content { | |
| flex: 1; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .grid-content iframe { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| border: none; | |
| } | |
| .loading { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(255, 255, 255, 0.8); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| font-size: 1.5rem; | |
| } | |
| .loading-spinner { | |
| border: 5px solid #f3f3f3; | |
| border-top: 5px solid #4CAF50; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @media (max-width: 768px) { | |
| .filter-controls { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .filter-controls > * { | |
| margin-bottom: 0.5rem; | |
| width: 100%; | |
| } | |
| .grid-container { | |
| grid-template-columns: 1fr; | |
| } | |
| input[type="text"] { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>์ธ๊ธฐ ํ๊น ํ์ด์ค ์คํ์ด์ค Top 100</h1> | |
| </div> | |
| <div class="filter-controls"> | |
| <input type="text" id="searchInput" placeholder="์ด๋ฆ, ํ๊ทธ ๋ฑ์ผ๋ก ๊ฒ์" /> | |
| <button id="refreshButton" class="refresh-btn"> | |
| <span class="refresh-icon"></span> | |
| ์๋ก๊ณ ์นจ | |
| </button> | |
| </div> | |
| <div id="gridContainer" class="grid-container"></div> | |
| </div> | |
| <div id="loadingIndicator" class="loading"> | |
| <div class="loading-spinner"></div> | |
| </div> | |
| <script> | |
| // DOM element references | |
| const elements = { | |
| gridContainer: document.getElementById('gridContainer'), | |
| loadingIndicator: document.getElementById('loadingIndicator'), | |
| searchInput: document.getElementById('searchInput'), | |
| refreshButton: document.getElementById('refreshButton') | |
| }; | |
| // Application state | |
| const state = { | |
| isLoading: false, | |
| spaces: [] | |
| }; | |
| // Display loading indicator | |
| function setLoading(isLoading) { | |
| state.isLoading = isLoading; | |
| elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
| if (isLoading) { | |
| elements.refreshButton.classList.add('refreshing'); | |
| } else { | |
| elements.refreshButton.classList.remove('refreshing'); | |
| } | |
| } | |
| // API error handling | |
| async function handleApiResponse(response) { | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`API ์ค๋ฅ (${response.status}): ${errorText}`); | |
| } | |
| return response.json(); | |
| } | |
| // Load spaces | |
| async function loadSpaces() { | |
| setLoading(true); | |
| try { | |
| const searchText = elements.searchInput.value; | |
| const response = await fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}`); | |
| const spaces = await handleApiResponse(response); | |
| state.spaces = spaces; | |
| renderGrid(spaces); | |
| } catch (error) { | |
| console.error('์คํ์ด์ค ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ:', error); | |
| alert(`์คํ์ด์ค ๋ก๋ ์ค๋ฅ: ${error.message}`); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // Render grid | |
| function renderGrid(spaces) { | |
| elements.gridContainer.innerHTML = ''; | |
| if (!spaces || spaces.length === 0) { | |
| const noResultsMsg = document.createElement('p'); | |
| noResultsMsg.textContent = 'ํ์ํ ์คํ์ด์ค๊ฐ ์์ต๋๋ค.'; | |
| noResultsMsg.style.padding = '1rem'; | |
| noResultsMsg.style.fontStyle = 'italic'; | |
| elements.gridContainer.appendChild(noResultsMsg); | |
| return; | |
| } | |
| spaces.forEach((item, index) => { | |
| const { url, embedUrl, title, likes_count, owner } = item; | |
| // Skip if owner is 'None' | |
| if (owner === 'None') { | |
| return; | |
| } | |
| // Create grid item | |
| const gridItem = document.createElement('div'); | |
| gridItem.className = 'grid-item'; | |
| // Header with title and actions | |
| const header = document.createElement('div'); | |
| header.className = 'grid-header'; | |
| // Left side of header (title and owner) | |
| const headerLeft = document.createElement('div'); | |
| headerLeft.className = 'grid-header-left'; | |
| const titleEl = document.createElement('h3'); | |
| titleEl.textContent = title; | |
| titleEl.title = title; // For tooltip on hover | |
| headerLeft.appendChild(titleEl); | |
| const ownerEl = document.createElement('div'); | |
| ownerEl.className = 'owner-info'; | |
| ownerEl.textContent = `by: ${owner}`; | |
| headerLeft.appendChild(ownerEl); | |
| header.appendChild(headerLeft); | |
| // Actions container | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'grid-actions'; | |
| // Likes count | |
| const likesCounter = document.createElement('div'); | |
| likesCounter.className = 'likes-counter'; | |
| likesCounter.innerHTML = 'โฅ <span>' + likes_count + '</span>'; | |
| actionsDiv.appendChild(likesCounter); | |
| // Open link | |
| const linkEl = document.createElement('a'); | |
| linkEl.href = url; | |
| linkEl.target = '_blank'; | |
| linkEl.className = 'open-link'; | |
| linkEl.textContent = '์ ์ฐฝ์์ ์ด๊ธฐ'; | |
| actionsDiv.appendChild(linkEl); | |
| header.appendChild(actionsDiv); | |
| // Add header to grid item | |
| gridItem.appendChild(header); | |
| // Content with iframe | |
| const content = document.createElement('div'); | |
| content.className = 'grid-content'; | |
| // Create iframe to display the content | |
| const iframe = document.createElement('iframe'); | |
| iframe.src = embedUrl; | |
| iframe.title = title; | |
| iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope; microphone; midi'; | |
| iframe.setAttribute('allowfullscreen', ''); | |
| iframe.setAttribute('frameborder', '0'); | |
| iframe.loading = 'lazy'; // Lazy load iframes for better performance | |
| content.appendChild(iframe); | |
| // Add content to grid item | |
| gridItem.appendChild(content); | |
| // Add grid item to container | |
| elements.gridContainer.appendChild(gridItem); | |
| }); | |
| } | |
| // Filter event listeners | |
| elements.searchInput.addEventListener('input', () => { | |
| // Debounce input to prevent API calls on every keystroke | |
| clearTimeout(state.searchTimeout); | |
| state.searchTimeout = setTimeout(loadSpaces, 300); | |
| }); | |
| // Refresh button event listener | |
| elements.refreshButton.addEventListener('click', loadSpaces); | |
| // Initialize | |
| loadSpaces(); | |
| </script> | |
| </body> | |
| </html> | |
| ''') | |
| # Use port 7860 for Huggingface Spaces | |
| app.run(host='0.0.0.0', port=7860) |