Spaces:
Running
Running
Commit
·
1256bd3
0
Parent(s):
Init
Browse files- .gitignore +2 -0
- Dockerfile +20 -0
- app/config.py +9 -0
- app/main.py +28 -0
- app/routes/auth.py +64 -0
- app/routes/youtube.py +77 -0
- app/templates/index.html +10 -0
- app/templates/success.html +11 -0
- requirements.txt +0 -0
.gitignore
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
venv
|
2 |
+
__pycache__
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use a lightweight Python image
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Create working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy requirements first for caching
|
8 |
+
COPY requirements.txt /app/
|
9 |
+
|
10 |
+
# Install dependencies
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Copy the rest of the application
|
14 |
+
COPY app /app/app
|
15 |
+
|
16 |
+
# Expose port (Hugging Face Spaces typically uses 7860 by default, but you can adjust if needed)
|
17 |
+
EXPOSE 7860
|
18 |
+
|
19 |
+
# Start the app with uvicorn
|
20 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app/config.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
# In a real environment, you would secure these properly.
|
4 |
+
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "50786072753-s9nq1ma3nv44k382b5mcnvmeggt1cvha.apps.googleusercontent.com")
|
5 |
+
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "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")
|
6 |
+
GOOGLE_REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "http://localhost:8000/auth/callback")
|
7 |
+
|
8 |
+
# For YouTube Data API. The user must consent to at least read their channel info and videos.
|
9 |
+
YOUTUBE_SCOPES = ["https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/youtube.force-ssl"]
|
app/main.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, Request
|
2 |
+
from fastapi.templating import Jinja2Templates
|
3 |
+
from fastapi.responses import HTMLResponse
|
4 |
+
from starlette.middleware.sessions import SessionMiddleware
|
5 |
+
import os
|
6 |
+
|
7 |
+
# Routers
|
8 |
+
from app.routes import auth, youtube
|
9 |
+
|
10 |
+
# For rendering HTML templates
|
11 |
+
templates = Jinja2Templates(directory="app/templates")
|
12 |
+
|
13 |
+
app = FastAPI()
|
14 |
+
|
15 |
+
# Set a secret key for session cookies (Use a strong key in production!)
|
16 |
+
app.add_middleware(SessionMiddleware, secret_key="CHANGE_THIS_SECRET")
|
17 |
+
|
18 |
+
# Include our routers
|
19 |
+
app.include_router(auth.router)
|
20 |
+
app.include_router(youtube.router)
|
21 |
+
|
22 |
+
@app.get("/", response_class=HTMLResponse)
|
23 |
+
async def read_root(request: Request):
|
24 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
25 |
+
|
26 |
+
@app.get("/success", response_class=HTMLResponse)
|
27 |
+
async def read_success(request: Request):
|
28 |
+
return templates.TemplateResponse("success.html", {"request": request})
|
app/routes/auth.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Request, Response, status
|
2 |
+
from fastapi.responses import RedirectResponse, HTMLResponse
|
3 |
+
from starlette.middleware.sessions import SessionMiddleware
|
4 |
+
from google_auth_oauthlib.flow import Flow
|
5 |
+
import os
|
6 |
+
|
7 |
+
from app.config import GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, YOUTUBE_SCOPES
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
|
12 |
+
def create_flow():
|
13 |
+
return Flow.from_client_config(
|
14 |
+
{
|
15 |
+
"web": {
|
16 |
+
"client_id": GOOGLE_CLIENT_ID,
|
17 |
+
"project_id": "youtube-fastapi-sample",
|
18 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
19 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
20 |
+
"client_secret": GOOGLE_CLIENT_SECRET,
|
21 |
+
}
|
22 |
+
},
|
23 |
+
scopes=YOUTUBE_SCOPES,
|
24 |
+
)
|
25 |
+
|
26 |
+
@router.get("/login")
|
27 |
+
async def login(request: Request):
|
28 |
+
flow = create_flow()
|
29 |
+
flow.redirect_uri = GOOGLE_REDIRECT_URI # must set the redirect_uri separately
|
30 |
+
authorization_url, state = flow.authorization_url(
|
31 |
+
access_type="offline",
|
32 |
+
include_granted_scopes="true",
|
33 |
+
prompt="select_account"
|
34 |
+
)
|
35 |
+
request.session["state"] = state
|
36 |
+
return RedirectResponse(authorization_url)
|
37 |
+
|
38 |
+
|
39 |
+
@router.get("/auth/callback")
|
40 |
+
async def auth_callback(request: Request):
|
41 |
+
"""Handle OAuth callback from Google with ?code= and ?state=."""
|
42 |
+
state = request.session.get("state")
|
43 |
+
if not state:
|
44 |
+
return HTMLResponse("<h1>Session state not found. Please /login again.</h1>", status_code=400)
|
45 |
+
|
46 |
+
flow = create_flow()
|
47 |
+
flow.fetch_token(authorization_response=str(request.url))
|
48 |
+
|
49 |
+
# Get the credentials object
|
50 |
+
credentials = flow.credentials
|
51 |
+
if not credentials or not credentials.valid:
|
52 |
+
return HTMLResponse("<h1>Invalid credentials. Please /login again.</h1>", status_code=400)
|
53 |
+
|
54 |
+
# Store credentials in session. In production, store securely (e.g. in DB, encrypted).
|
55 |
+
request.session["credentials"] = {
|
56 |
+
"token": credentials.token,
|
57 |
+
"refresh_token": credentials.refresh_token,
|
58 |
+
"token_uri": credentials.token_uri,
|
59 |
+
"client_id": credentials.client_id,
|
60 |
+
"client_secret": credentials.client_secret,
|
61 |
+
"scopes": credentials.scopes
|
62 |
+
}
|
63 |
+
|
64 |
+
return RedirectResponse(url="/success", status_code=status.HTTP_302_FOUND)
|
app/routes/youtube.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Request, HTTPException
|
2 |
+
from google.oauth2.credentials import Credentials
|
3 |
+
from googleapiclient.discovery import build
|
4 |
+
|
5 |
+
router = APIRouter()
|
6 |
+
|
7 |
+
def get_credentials_from_session(session) -> Credentials:
|
8 |
+
"""Utility to build a Credentials object from stored session data."""
|
9 |
+
creds_data = session.get("credentials")
|
10 |
+
if not creds_data:
|
11 |
+
return None
|
12 |
+
return Credentials(
|
13 |
+
token=creds_data["token"],
|
14 |
+
refresh_token=creds_data["refresh_token"],
|
15 |
+
token_uri=creds_data["token_uri"],
|
16 |
+
client_id=creds_data["client_id"],
|
17 |
+
client_secret=creds_data["client_secret"],
|
18 |
+
scopes=creds_data["scopes"]
|
19 |
+
)
|
20 |
+
|
21 |
+
@router.get("/list-channels")
|
22 |
+
async def list_channels(request: Request):
|
23 |
+
"""Return the user’s list of YouTube channels."""
|
24 |
+
creds = get_credentials_from_session(request.session)
|
25 |
+
if not creds or not creds.valid:
|
26 |
+
raise HTTPException(status_code=401, detail="Unauthorized. Please /login first.")
|
27 |
+
|
28 |
+
youtube = build("youtube", "v3", credentials=creds)
|
29 |
+
response = youtube.channels().list(
|
30 |
+
part="id,snippet",
|
31 |
+
mine=True
|
32 |
+
).execute()
|
33 |
+
|
34 |
+
channels = []
|
35 |
+
for item in response.get("items", []):
|
36 |
+
channels.append({
|
37 |
+
"channelId": item["id"],
|
38 |
+
"title": item["snippet"]["title"]
|
39 |
+
})
|
40 |
+
|
41 |
+
return {"channels": channels}
|
42 |
+
|
43 |
+
|
44 |
+
@router.get("/list-videos")
|
45 |
+
async def list_videos(request: Request, channel_id: str):
|
46 |
+
"""List videos for the specified channel."""
|
47 |
+
creds = get_credentials_from_session(request.session)
|
48 |
+
if not creds or not creds.valid:
|
49 |
+
raise HTTPException(status_code=401, detail="Unauthorized. Please /login first.")
|
50 |
+
|
51 |
+
youtube = build("youtube", "v3", credentials=creds)
|
52 |
+
|
53 |
+
# Example: listing videos from a channel’s "uploads" playlist
|
54 |
+
# 1) Retrieve the uploads playlist from channel
|
55 |
+
channel_response = youtube.channels().list(
|
56 |
+
part="contentDetails",
|
57 |
+
id=channel_id
|
58 |
+
).execute()
|
59 |
+
|
60 |
+
uploads_playlist_id = channel_response["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
|
61 |
+
|
62 |
+
# 2) Retrieve items from the uploads playlist
|
63 |
+
playlist_items_response = youtube.playlistItems().list(
|
64 |
+
part="snippet",
|
65 |
+
playlistId=uploads_playlist_id,
|
66 |
+
maxResults=10
|
67 |
+
).execute()
|
68 |
+
|
69 |
+
videos = []
|
70 |
+
for item in playlist_items_response.get("items", []):
|
71 |
+
snippet = item["snippet"]
|
72 |
+
videos.append({
|
73 |
+
"videoId": snippet["resourceId"]["videoId"],
|
74 |
+
"title": snippet["title"]
|
75 |
+
})
|
76 |
+
|
77 |
+
return {"videos": videos}
|
app/templates/index.html
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<title>FastAPI + Google OAuth2 Example</title>
|
5 |
+
</head>
|
6 |
+
<body>
|
7 |
+
<h1>Welcome to the FastAPI + Google OAuth2 Example</h1>
|
8 |
+
<p>Please <a href="/login">Login with Google</a> to see your YouTube channels.</p>
|
9 |
+
</body>
|
10 |
+
</html>
|
app/templates/success.html
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<title>Login Successful</title>
|
5 |
+
</head>
|
6 |
+
<body>
|
7 |
+
<h1>Login Successful!</h1>
|
8 |
+
<p>You can now view your <a href="/list-channels" target="_blank">YouTube Channels (JSON)</a>.</p>
|
9 |
+
<p>After you get a channel ID, you can try <code>/list-videos?channel_id=XYZ</code> to see its videos.</p>
|
10 |
+
</body>
|
11 |
+
</html>
|
requirements.txt
ADDED
Binary file (1.54 kB). View file
|
|