Spaces:
Running
Running
feat: youTube API servisi ve backend routing için video interaction ve comment management eklendi
Browse files- app/main.py +2 -3
- app/routes/analyze.py +1 -1
- app/routes/commend/router.py +4 -1
- app/routes/commend/youtube_service.py +8 -11
- app/routes/data_processing.py +9 -3
app/main.py
CHANGED
|
@@ -86,10 +86,9 @@ _origins = _load_origins()
|
|
| 86 |
app.add_middleware(
|
| 87 |
CORSMiddleware,
|
| 88 |
allow_origins=_origins,
|
| 89 |
-
# credentials=True is only safe with an explicit origin list, not wildcard
|
| 90 |
allow_credentials="*" not in _origins,
|
| 91 |
-
allow_methods=["
|
| 92 |
-
allow_headers=["
|
| 93 |
)
|
| 94 |
|
| 95 |
app.include_router(health_router)
|
|
|
|
| 86 |
app.add_middleware(
|
| 87 |
CORSMiddleware,
|
| 88 |
allow_origins=_origins,
|
|
|
|
| 89 |
allow_credentials="*" not in _origins,
|
| 90 |
+
allow_methods=["GET", "POST", "OPTIONS"],
|
| 91 |
+
allow_headers=["Content-Type", "Accept"],
|
| 92 |
)
|
| 93 |
|
| 94 |
app.include_router(health_router)
|
app/routes/analyze.py
CHANGED
|
@@ -62,7 +62,7 @@ async def _analyze_rate_limit(request: Request) -> None:
|
|
| 62 |
if len(_analyze_rate_store) > _ANALYZE_MAX_IPS:
|
| 63 |
stale = [
|
| 64 |
ip for ip, ts in _analyze_rate_store.items()
|
| 65 |
-
if all(t <= cutoff for t in ts)
|
| 66 |
]
|
| 67 |
for ip in stale:
|
| 68 |
del _analyze_rate_store[ip]
|
|
|
|
| 62 |
if len(_analyze_rate_store) > _ANALYZE_MAX_IPS:
|
| 63 |
stale = [
|
| 64 |
ip for ip, ts in _analyze_rate_store.items()
|
| 65 |
+
if not ts or all(t <= cutoff for t in ts)
|
| 66 |
]
|
| 67 |
for ip in stale:
|
| 68 |
del _analyze_rate_store[ip]
|
app/routes/commend/router.py
CHANGED
|
@@ -131,7 +131,10 @@ def _evict_stale_ips() -> None:
|
|
| 131 |
if len(_rate_limit_store) <= _MAX_TRACKED_IPS:
|
| 132 |
return
|
| 133 |
cutoff = time.time() - _RATE_LIMIT_WINDOW
|
| 134 |
-
stale_keys = [
|
|
|
|
|
|
|
|
|
|
| 135 |
for key in stale_keys:
|
| 136 |
del _rate_limit_store[key]
|
| 137 |
|
|
|
|
| 131 |
if len(_rate_limit_store) <= _MAX_TRACKED_IPS:
|
| 132 |
return
|
| 133 |
cutoff = time.time() - _RATE_LIMIT_WINDOW
|
| 134 |
+
stale_keys = [
|
| 135 |
+
ip for ip, ts in _rate_limit_store.items()
|
| 136 |
+
if not ts or all(t <= cutoff for t in ts)
|
| 137 |
+
]
|
| 138 |
for key in stale_keys:
|
| 139 |
del _rate_limit_store[key]
|
| 140 |
|
app/routes/commend/youtube_service.py
CHANGED
|
@@ -18,6 +18,7 @@ from urllib.parse import urlparse
|
|
| 18 |
from google.oauth2.credentials import Credentials
|
| 19 |
from google.auth.transport.requests import Request
|
| 20 |
from googleapiclient.discovery import build
|
|
|
|
| 21 |
import isodate
|
| 22 |
|
| 23 |
from ...services.logging_config import get_logger
|
|
@@ -25,11 +26,11 @@ from ...services.logging_config import get_logger
|
|
| 25 |
logger = get_logger(__name__)
|
| 26 |
|
| 27 |
# Cached service instances
|
| 28 |
-
_api_key_service = None
|
| 29 |
-
_oauth_service = None
|
| 30 |
|
| 31 |
|
| 32 |
-
def _get_api_key_service():
|
| 33 |
"""
|
| 34 |
API Key ile YouTube API servisi oluşturur.
|
| 35 |
Read-only işlemler için kullanılır (video detayları, yorumları okuma).
|
|
@@ -56,16 +57,12 @@ def _get_api_key_service():
|
|
| 56 |
raise ValueError(f"YouTube API initialization failed: {e}")
|
| 57 |
|
| 58 |
|
| 59 |
-
def _get_oauth_service():
|
| 60 |
"""
|
| 61 |
OAuth 2.0 ile YouTube API servisi oluşturur.
|
| 62 |
Write işlemleri için kullanılır (yorum gönderme).
|
|
|
|
| 63 |
"""
|
| 64 |
-
global _oauth_service
|
| 65 |
-
|
| 66 |
-
if _oauth_service is not None:
|
| 67 |
-
# Check if credentials are still valid
|
| 68 |
-
return _oauth_service
|
| 69 |
|
| 70 |
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
|
| 71 |
|
|
@@ -82,9 +79,9 @@ def _get_oauth_service():
|
|
| 82 |
logger.info("Refreshing expired OAuth token")
|
| 83 |
creds.refresh(Request())
|
| 84 |
|
| 85 |
-
|
| 86 |
logger.info("YouTube OAuth service initialized")
|
| 87 |
-
return
|
| 88 |
except json.JSONDecodeError as e:
|
| 89 |
logger.error(f"TOKEN_JSON is not valid JSON: {e}")
|
| 90 |
raise ValueError("YouTube OAuth token is malformed")
|
|
|
|
| 18 |
from google.oauth2.credentials import Credentials
|
| 19 |
from google.auth.transport.requests import Request
|
| 20 |
from googleapiclient.discovery import build
|
| 21 |
+
from googleapiclient.errors import HttpError
|
| 22 |
import isodate
|
| 23 |
|
| 24 |
from ...services.logging_config import get_logger
|
|
|
|
| 26 |
logger = get_logger(__name__)
|
| 27 |
|
| 28 |
# Cached service instances
|
| 29 |
+
_api_key_service: Any = None
|
| 30 |
+
_oauth_service: Any = None
|
| 31 |
|
| 32 |
|
| 33 |
+
def _get_api_key_service() -> Any:
|
| 34 |
"""
|
| 35 |
API Key ile YouTube API servisi oluşturur.
|
| 36 |
Read-only işlemler için kullanılır (video detayları, yorumları okuma).
|
|
|
|
| 57 |
raise ValueError(f"YouTube API initialization failed: {e}")
|
| 58 |
|
| 59 |
|
| 60 |
+
def _get_oauth_service() -> Any:
|
| 61 |
"""
|
| 62 |
OAuth 2.0 ile YouTube API servisi oluşturur.
|
| 63 |
Write işlemleri için kullanılır (yorum gönderme).
|
| 64 |
+
Credentials are created fresh each call to avoid token reuse issues.
|
| 65 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
|
| 68 |
|
|
|
|
| 79 |
logger.info("Refreshing expired OAuth token")
|
| 80 |
creds.refresh(Request())
|
| 81 |
|
| 82 |
+
service = build('youtube', 'v3', credentials=creds)
|
| 83 |
logger.info("YouTube OAuth service initialized")
|
| 84 |
+
return service
|
| 85 |
except json.JSONDecodeError as e:
|
| 86 |
logger.error(f"TOKEN_JSON is not valid JSON: {e}")
|
| 87 |
raise ValueError("YouTube OAuth token is malformed")
|
app/routes/data_processing.py
CHANGED
|
@@ -3,6 +3,7 @@ Routes for data processing and manipulation (Audio/Image).
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
import time
|
|
|
|
| 6 |
from collections import defaultdict
|
| 7 |
from fastapi import APIRouter, File, UploadFile, Form, HTTPException, Request, Depends, status
|
| 8 |
from fastapi.responses import StreamingResponse
|
|
@@ -29,7 +30,7 @@ async def _process_rate_limit(request: Request) -> None:
|
|
| 29 |
cutoff = now - _PROCESS_RATE_WINDOW
|
| 30 |
|
| 31 |
if len(_process_rate_store) > _PROCESS_MAX_IPS:
|
| 32 |
-
stale = [ip for ip, ts in _process_rate_store.items() if all(t <= cutoff for t in ts)]
|
| 33 |
for ip in stale:
|
| 34 |
del _process_rate_store[ip]
|
| 35 |
|
|
@@ -72,6 +73,11 @@ async def process_audio_endpoint(
|
|
| 72 |
raise HTTPException(status_code=400, detail={"code": "invalid_file_type", "message": "Invalid file type. Must be audio."})
|
| 73 |
|
| 74 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Read file in chunks to enforce size limit before full allocation
|
| 76 |
chunks = []
|
| 77 |
total_read = 0
|
|
@@ -90,11 +96,11 @@ async def process_audio_endpoint(
|
|
| 90 |
processed_audio = process_audio(content, parsed_options)
|
| 91 |
|
| 92 |
# Return as downloadable file
|
| 93 |
-
|
| 94 |
return StreamingResponse(
|
| 95 |
processed_audio,
|
| 96 |
media_type="audio/wav",
|
| 97 |
-
headers={"Content-Disposition": f"attachment; filename={
|
| 98 |
)
|
| 99 |
|
| 100 |
except ValueError as e:
|
|
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
import time
|
| 6 |
+
import re
|
| 7 |
from collections import defaultdict
|
| 8 |
from fastapi import APIRouter, File, UploadFile, Form, HTTPException, Request, Depends, status
|
| 9 |
from fastapi.responses import StreamingResponse
|
|
|
|
| 30 |
cutoff = now - _PROCESS_RATE_WINDOW
|
| 31 |
|
| 32 |
if len(_process_rate_store) > _PROCESS_MAX_IPS:
|
| 33 |
+
stale = [ip for ip, ts in _process_rate_store.items() if not ts or all(t <= cutoff for t in ts)]
|
| 34 |
for ip in stale:
|
| 35 |
del _process_rate_store[ip]
|
| 36 |
|
|
|
|
| 73 |
raise HTTPException(status_code=400, detail={"code": "invalid_file_type", "message": "Invalid file type. Must be audio."})
|
| 74 |
|
| 75 |
try:
|
| 76 |
+
# Sanitize filename to prevent injection attacks
|
| 77 |
+
safe_filename = re.sub(r'[^a-zA-Z0-9._-]', '_', file.filename or 'audio')
|
| 78 |
+
if safe_filename.endswith('.wav'):
|
| 79 |
+
safe_filename = safe_filename[:-4]
|
| 80 |
+
|
| 81 |
# Read file in chunks to enforce size limit before full allocation
|
| 82 |
chunks = []
|
| 83 |
total_read = 0
|
|
|
|
| 96 |
processed_audio = process_audio(content, parsed_options)
|
| 97 |
|
| 98 |
# Return as downloadable file
|
| 99 |
+
output_filename = f"processed_{safe_filename}.wav"
|
| 100 |
return StreamingResponse(
|
| 101 |
processed_audio,
|
| 102 |
media_type="audio/wav",
|
| 103 |
+
headers={"Content-Disposition": f"attachment; filename={output_filename}"}
|
| 104 |
)
|
| 105 |
|
| 106 |
except ValueError as e:
|