Skip to content

Commit 0ee9356

Browse files
committed
Add YouTube API integration and routes
Introduce a new YouTube integration: adds app/api/youtube package with routes and SQL helpers. Includes GET /youtube (returns counts and recent rows), POST /youtube/createtable (creates youtube_channels, youtube_videos, youtube_playlists, youtube_resources), POST /youtube/emptytables (clears those tables), and POST /youtube/sync (fetches channel and latest videos via YouTube Data API and stores them). Also adds README, registers youtube_router in app/api/routes.py, updates .env.sample with YOUTUBE_API_KEY and YOUTUBE_CHANNEL_ID, and increases Flickr sync per_page from 10 to 100.
1 parent ebd170e commit 0ee9356

9 files changed

Lines changed: 266 additions & 3 deletions

File tree

.env.sample

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ DB_USER=
1010
DB_PASSWORD=
1111
FLICKR_USER=@N00
1212
FLICKR_KEY=
13-
FLICKR_SECRET=
13+
FLICKR_SECRET=
14+
YOUTUBE_API_KEY=
15+
YOUTUBE_CHANNEL_ID=

app/api/flickr/sql/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def sync_flickr(api_key: str = Depends(get_api_key)) -> dict:
2727
"user_id": flickr_user,
2828
"format": "json",
2929
"nojsoncallback": 1,
30-
"per_page": 10
30+
"per_page": 100
3131
}
3232
try:
3333
resp = requests.get(url, params=params)

app/api/routes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from app.api.github import github_router
1919
from app.api.flickr import flickr_router
20+
from app.api.youtube import youtube_router
2021

2122
router.include_router(root_router)
2223
router.include_router(resend_router)
@@ -28,4 +29,5 @@
2829
router.include_router(orders_router)
2930
router.include_router(queue_router)
3031
router.include_router(github_router)
31-
router.include_router(flickr_router)
32+
router.include_router(flickr_router)
33+
router.include_router(youtube_router)

app/api/youtube/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# YouTube API Integration
2+
3+
This module provides API routes for accessing YouTube channel data, similar to the GitHub and Flickr integrations. It expects the following environment variable in your `.env` file:
4+
5+
- YOUTUBE_API_KEY
6+
7+
## Endpoints
8+
9+
- **GET /youtube**: Returns counts and recent records from all YouTube tables.
10+
11+
## Proposed Table Design
12+
13+
1. youtube_channels
14+
- One row per YouTube channel.
15+
- Stores channel identity fields and full raw payload.
16+
2. youtube_videos
17+
- One row per video.
18+
- Stores video metadata plus raw JSON payload.
19+
3. youtube_playlists
20+
- One row per playlist.
21+
- Stores playlist metadata plus raw JSON payload.
22+
4. youtube_resources
23+
- Generic catch-all for any future YouTube resource type.
24+
- Supports additional YouTube objects through jsonb payload storage.
25+
26+
This structure mirrors the GitHub and Flickr integrations for consistency and flexibility.

app/api/youtube/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""YouTube Routes"""
2+
3+
from fastapi import APIRouter
4+
5+
from .youtube import router as _youtube_router
6+
from .sql.create_tables import router as _create_tables_router
7+
from .sql.empty_tables import router as _empty_tables_router
8+
from .sql.sync import router as _sync_router
9+
10+
youtube_router = APIRouter()
11+
youtube_router.include_router(_youtube_router)
12+
youtube_router.include_router(_create_tables_router)
13+
youtube_router.include_router(_empty_tables_router)
14+
youtube_router.include_router(_sync_router)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from fastapi import APIRouter, Depends
2+
from app.utils.db import get_db_connection_direct
3+
from app.utils.make_meta import make_meta
4+
from app.utils.api_key_auth import get_api_key
5+
6+
router = APIRouter()
7+
8+
@router.post("/youtube/createtable")
9+
def create_youtube_tables(api_key: str = Depends(get_api_key)) -> dict:
10+
"""POST /youtube/createtable: Drop and create YouTube tables in Postgres."""
11+
sql_statements = [
12+
'DROP TABLE IF EXISTS youtube_resources;',
13+
'DROP TABLE IF EXISTS youtube_playlists;',
14+
'DROP TABLE IF EXISTS youtube_videos;',
15+
'DROP TABLE IF EXISTS youtube_channels;',
16+
'''CREATE TABLE IF NOT EXISTS youtube_channels (
17+
id SERIAL PRIMARY KEY,
18+
youtube_id TEXT UNIQUE,
19+
title TEXT,
20+
payload JSONB,
21+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
22+
);''',
23+
'''CREATE TABLE IF NOT EXISTS youtube_videos (
24+
id SERIAL PRIMARY KEY,
25+
youtube_id TEXT UNIQUE,
26+
title TEXT,
27+
payload JSONB,
28+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
29+
);''',
30+
'''CREATE TABLE IF NOT EXISTS youtube_playlists (
31+
id SERIAL PRIMARY KEY,
32+
youtube_id TEXT,
33+
title TEXT,
34+
payload JSONB,
35+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
36+
);''',
37+
'''CREATE TABLE IF NOT EXISTS youtube_resources (
38+
id SERIAL PRIMARY KEY,
39+
resource_type TEXT,
40+
youtube_id TEXT,
41+
payload JSONB,
42+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
43+
);'''
44+
]
45+
conn = None
46+
cur = None
47+
try:
48+
conn = get_db_connection_direct()
49+
cur = conn.cursor()
50+
for stmt in sql_statements:
51+
cur.execute(stmt)
52+
conn.commit()
53+
return {"meta": make_meta("success", "YouTube tables created"), "data": {}}
54+
except Exception as e:
55+
return {"meta": make_meta("error", f"DB error: {str(e)}"), "data": {}}
56+
finally:
57+
if cur is not None:
58+
cur.close()
59+
if conn is not None:
60+
conn.close()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import APIRouter, Depends
2+
from app.utils.db import get_db_connection_direct
3+
from app.utils.make_meta import make_meta
4+
from app.utils.api_key_auth import get_api_key
5+
6+
router = APIRouter()
7+
8+
@router.post("/youtube/emptytables")
9+
def empty_youtube_tables(api_key: str = Depends(get_api_key)) -> dict:
10+
"""POST /youtube/emptytables: Delete all rows from all YouTube tables."""
11+
tables = [
12+
"youtube_channels",
13+
"youtube_videos",
14+
"youtube_playlists",
15+
"youtube_resources"
16+
]
17+
conn = None
18+
cur = None
19+
try:
20+
conn = get_db_connection_direct()
21+
cur = conn.cursor()
22+
for table in tables:
23+
cur.execute(f"DELETE FROM {table};")
24+
conn.commit()
25+
return {"meta": make_meta("success", "YouTube tables emptied"), "data": {}}
26+
except Exception as e:
27+
return {"meta": make_meta("error", f"DB error: {str(e)}"), "data": {}}
28+
finally:
29+
if cur is not None:
30+
cur.close()
31+
if conn is not None:
32+
conn.close()

app/api/youtube/sql/sync.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from fastapi import APIRouter, Depends
2+
from app.utils.db import get_db_connection_direct
3+
from app.utils.make_meta import make_meta
4+
from app.utils.api_key_auth import get_api_key
5+
import os
6+
import requests
7+
import json
8+
from dotenv import load_dotenv
9+
10+
router = APIRouter()
11+
12+
@router.post("/youtube/sync")
13+
def sync_youtube(api_key: str = Depends(get_api_key)) -> dict:
14+
"""POST /youtube/sync: Fetches data from YouTube Data API and stores in DB."""
15+
load_dotenv()
16+
youtube_key = os.getenv("YOUTUBE_API_KEY")
17+
if not youtube_key:
18+
return {"meta": make_meta("error", "Missing YouTube API key"), "data": {}}
19+
20+
# Example: Fetch channel info and latest videos for a given channel ID
21+
channel_id = os.getenv("YOUTUBE_CHANNEL_ID") # Optionally add this to .env
22+
if not channel_id:
23+
return {"meta": make_meta("error", "Missing YOUTUBE_CHANNEL_ID in .env"), "data": {}}
24+
25+
try:
26+
# Fetch channel details
27+
channel_url = "https://www.googleapis.com/youtube/v3/channels"
28+
channel_params = {
29+
"part": "snippet,contentDetails,statistics",
30+
"id": channel_id,
31+
"key": youtube_key
32+
}
33+
channel_resp = requests.get(channel_url, params=channel_params)
34+
channel_resp.raise_for_status()
35+
channel_data = channel_resp.json()
36+
channel_items = channel_data.get("items", [])
37+
if channel_items:
38+
channel = channel_items[0]
39+
else:
40+
return {"meta": make_meta("error", "Channel not found"), "data": {}}
41+
42+
# Insert channel info
43+
conn = get_db_connection_direct()
44+
cur = conn.cursor()
45+
cur.execute(
46+
"""
47+
INSERT INTO youtube_channels (youtube_id, title, payload)
48+
VALUES (%s, %s, %s)
49+
ON CONFLICT (youtube_id) DO NOTHING;
50+
""",
51+
(channel.get("id"), channel["snippet"].get("title"), json.dumps(channel))
52+
)
53+
54+
# Fetch latest videos from uploads playlist
55+
uploads_playlist_id = channel["contentDetails"]["relatedPlaylists"]["uploads"]
56+
playlist_url = "https://www.googleapis.com/youtube/v3/playlistItems"
57+
playlist_params = {
58+
"part": "snippet,contentDetails",
59+
"playlistId": uploads_playlist_id,
60+
"maxResults": 100,
61+
"key": youtube_key
62+
}
63+
playlist_resp = requests.get(playlist_url, params=playlist_params)
64+
playlist_resp.raise_for_status()
65+
playlist_data = playlist_resp.json()
66+
videos = playlist_data.get("items", [])
67+
for video in videos:
68+
video_id = video["contentDetails"]["videoId"]
69+
title = video["snippet"].get("title")
70+
cur.execute(
71+
"""
72+
INSERT INTO youtube_videos (youtube_id, title, payload)
73+
VALUES (%s, %s, %s)
74+
ON CONFLICT (youtube_id) DO NOTHING;
75+
""",
76+
(video_id, title, json.dumps(video))
77+
)
78+
conn.commit()
79+
cur.close()
80+
conn.close()
81+
return {"meta": make_meta("success", f"Synced {len(videos)} videos from YouTube"), "data": {"count": len(videos)}}
82+
except Exception as e:
83+
return {"meta": make_meta("error", f"Sync error: {str(e)}"), "data": {}}

app/api/youtube/youtube.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from fastapi import APIRouter, Depends
2+
from app.utils.make_meta import make_meta
3+
from app.utils.db import get_db_connection_direct
4+
from app.utils.api_key_auth import get_api_key
5+
6+
router = APIRouter()
7+
8+
_TABLES = [
9+
"youtube_channels",
10+
"youtube_videos",
11+
"youtube_playlists",
12+
"youtube_resources",
13+
]
14+
15+
def _fetch_table(cur, table: str) -> dict:
16+
cur.execute(f"SELECT COUNT(*) FROM {table};")
17+
row = cur.fetchone()
18+
count = row[0] if row and row[0] is not None else 0
19+
cur.execute(f"SELECT * FROM {table} ORDER BY id DESC LIMIT 100;")
20+
if cur.description:
21+
columns = [desc[0] for desc in cur.description]
22+
rows = [dict(zip(columns, r)) for r in cur.fetchall()]
23+
else:
24+
rows = []
25+
return {"count": count, "rows": rows}
26+
27+
28+
@router.get("/youtube")
29+
def get_youtube(api_key: str = Depends(get_api_key)) -> dict:
30+
"""GET /youtube: Return counts and records from all YouTube tables."""
31+
conn = None
32+
cur = None
33+
try:
34+
conn = get_db_connection_direct()
35+
cur = conn.cursor()
36+
data = {table: _fetch_table(cur, table) for table in _TABLES}
37+
return {"meta": make_meta("success", "YouTube data"), "data": data}
38+
except Exception as e:
39+
return {"meta": make_meta("error", f"DB error: {str(e)}"), "data": {}}
40+
finally:
41+
if cur is not None:
42+
cur.close()
43+
if conn is not None:
44+
conn.close()

0 commit comments

Comments
 (0)