aliSaac510 commited on
Commit
9028687
·
verified ·
1 Parent(s): ee2cfc1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -174
app.py CHANGED
@@ -1,187 +1,122 @@
1
- from fastapi import FastAPI, Query, HTTPException
2
- import httpx
3
- from bs4 import BeautifulSoup
4
- import json
5
  import re
6
- from typing import List, Dict, Any
 
 
 
 
 
 
7
 
8
  app = FastAPI()
9
 
10
- async def _extract_videos_from_html(html_content: str) -> List[Dict[str, Any]]:
 
 
 
11
  """
12
- Helper function to parse YouTube HTML content and extract video data.
 
 
 
 
 
 
 
 
 
13
  """
14
- soup = BeautifulSoup(html_content, 'html.parser')
15
-
16
- # Set headers to mimic a browser to reduce blocking
17
- headers = {
18
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
19
- "Accept-Language": "en-US,en;q=0.9",
20
- }
21
-
22
- script_tags = soup.find_all('script')
23
- yt_initial_data = None
24
- for script in script_tags:
25
- if script.string and 'var ytInitialData = ' in script.string:
26
- json_str = script.string.split('var ytInitialData = ', 1)[1]
27
-
28
- # Robust JSON extraction:
29
- # YouTube's JSON can sometimes have a trailing semicolon followed by other JS.
30
- # We need to ensure we parse only the JSON object.
31
- # Find the balance of curly braces to identify the end of the JSON.
32
- balance = 0
33
- json_end_index = -1
34
- # Simple heuristic to find balanced brace for the main JSON object
35
- # This might need more sophistication for edge cases
36
- for i, char in enumerate(json_str):
37
- if char == '{':
38
- balance += 1
39
- elif char == '}':
40
- balance -= 1
41
-
42
- if balance == 0 and char == '}':
43
- json_end_index = i
44
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- if json_end_index != -1:
47
- json_str = json_str[:json_end_index + 1]
48
- else: # Fallback if balancing failed, try splitting by semicolon
49
- json_str = json_str.split(');', 1)[0]
 
 
 
50
 
51
 
52
- try:
53
- yt_initial_data = json.loads(json_str)
 
 
 
 
 
54
  break
55
- except json.JSONDecodeError as e:
56
- print(f"DEBUG: JSON Decode Error while parsing ytInitialData: {e}")
57
- # print(f"DEBUG: Attempted JSON string (start): {json_str[:500]}...") # Too verbose for production
58
- # print(f"DEBUG: Attempted JSON string (end): ...{json_str[-500:]}") # Too verbose for production
59
- yt_initial_data = None # Reset to try next script tag if any
60
 
61
- if not yt_initial_data:
62
- print("DEBUG: ytInitialData not found or could not be parsed from any script tag.")
63
- return []
64
 
65
- # Debugging: Print a snippet of the parsed ytInitialData structure (use sparingly)
66
- # print(f"DEBUG: First 1000 chars of ytInitialData (for structure inspection): {json.dumps(yt_initial_data, indent=2)[:1000]}...")
 
67
 
68
- raw_videos_data = []
69
- try:
70
- # Common path for video renderers in search results
71
- # This path is subject to YouTube UI changes. We try multiple known paths.
72
- contents = yt_initial_data.get('contents', {})
73
-
74
- # Path 1: twoColumnSearchResultsRenderer -> primaryContents -> sectionListRenderer
75
- sections = contents.get('twoColumnSearchResultsRenderer', {})\
76
- .get('primaryContents', {})\
77
- .get('sectionListRenderer', {})\
78
- .get('contents', [])
79
-
80
- # Path 2: Fallback for different structures (e.g., singleColumnBrowseResultsRenderer for hashtag pages)
81
- if not sections:
82
- # This path is more common for hashtag pages or channels that are directly linked
83
- sections = contents.get('singleColumnBrowseResultsRenderer', {})\
84
- .get('tabs', [{}])[0]\
85
- .get('tabRenderer', {})\
86
- .get('content', {})\
87
- .get('sectionListRenderer', {})\
88
- .get('contents', [])
89
- if not sections:
90
- # Another common path for some layouts
91
- sections = contents.get('twoColumnBrowseResultsRenderer', {})\
92
- .get('tabs', [{}])[0]\
93
- .get('tabRenderer', {})\
94
- .get('content', {})\
95
- .get('sectionListRenderer', {})\
96
- .get('contents', [])
97
-
98
-
99
- for section in sections:
100
- item_section_renderer = section.get('itemSectionRenderer', {})
101
- for item in item_section_renderer.get('contents', []):
102
- if 'videoRenderer' in item:
103
- video_data = item['videoRenderer']
104
-
105
- extracted_data = {
106
- "id": video_data.get('videoId'),
107
- "title": video_data.get('title', {}).get('runs', [{}])[0].get('text'),
108
- "description": video_data.get('descriptionSnippet', {}).get('runs', [{}])[0].get('text'),
109
- "duration_text": video_data.get('lengthText', {}).get('simpleText'),
110
- "view_count": video_data.get('viewCountText', {}).get('simpleText'),
111
- "published_time": video_data.get('publishedTimeText', {}).get('simpleText'),
112
- "channel_name": video_data.get('ownerText', {}).get('runs', [{}])[0].get('text'),
113
- "channel_id": video_data.get('ownerText', {}).get('runs', [{}])[0].get('navigationEndpoint', {}).get('browseEndpoint', {}).get('browseId'),
114
- "thumbnails": [thumb.get('url') for thumb in video_data.get('thumbnail', {}).get('thumbnails', [])],
115
- "url": f"https://www.youtube.com/watch?v={video_data.get('videoId')}" if video_data.get('videoId') else None,
116
- "badges": [badge.get('metadataBadgeRenderer', {}).get('label') for badge in video_data.get('badges', []) if 'metadataBadgeRenderer' in badge],
117
- "is_short_url": "shorts/" in (f"https://www.youtube.com/watch?v={video_data.get('videoId')}" if video_data.get('videoId') else "") # Indication if URL is a shorts URL
118
- }
119
- raw_videos_data.append(extracted_data)
120
-
121
- except KeyError as ke:
122
- print(f"DEBUG: YouTube data structure changed (KeyError) during extraction: {ke}")
123
- return raw_videos_data
124
- except IndexError as ie:
125
- print(f"DEBUG: YouTube data structure changed (IndexError) during extraction: {ie}")
126
- return raw_videos_data
127
- except Exception as data_extract_e:
128
- print(f"DEBUG: An unexpected error occurred during data extraction: {data_extract_e}")
129
- return raw_videos_data
130
-
131
- return raw_videos_data
132
-
133
-
134
- @app.get("/scrape_raw_shorts")
135
- async def scrape_raw_youtube_shorts(
136
- query: str = Query(..., description="The search query for YouTube Shorts")
137
- ) -> List[Dict[str, Any]]:
138
- if not query:
139
- raise HTTPException(status_code=400, detail="'query' parameter is required.")
140
-
141
- all_shorts_combined = []
142
- seen_video_ids = set()
143
-
144
- # Common headers to mimic a browser
145
- headers = {
146
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
147
- "Accept-Language": "en-US,en;q=0.9",
148
- }
149
-
150
- async with httpx.AsyncClient(headers=headers) as client:
151
- # --- Method 1: Original Search Query --- #
152
- print(f"\n--- Processing results from Search Query: {query} ---") # Separator for Method 1
153
- search_url_1 = f"https://www.youtube.com/results?search_query={query}&sp=EgIQCQ%253D%253D"
154
- try:
155
- response_1 = await client.get(search_url_1)
156
- response_1.raise_for_status()
157
- shorts_from_search = await _extract_videos_from_html(response_1.text)
158
- print(f"DEBUG: Found {len(shorts_from_search)} potential shorts from Search Query.") # Debugging count
159
- for short in shorts_from_search:
160
- if short and short.get('id') and short['id'] not in seen_video_ids:
161
- all_shorts_combined.append(short)
162
- seen_video_ids.add(short['id'])
163
- except (httpx.HTTPStatusError, httpx.RequestError, Exception) as e:
164
- print(f"ERROR: Error scraping from search_query ({search_url_1}): {e}")
165
-
166
- # --- Method 2: Hashtag Search --- #
167
- print(f"\n--- Processing results from Hashtag Search: {query} ---") # Separator for Method 2
168
- query_hashtag = query.replace(" ", "_").replace("'", "").replace("(", "").replace(")", "").replace(".", "").lower() # Clean query for hashtag
169
- # Ensure hashtag query is not empty after cleaning
170
- if query_hashtag:
171
- search_url_2 = f"https://www.youtube.com/hashtag/{query_hashtag}/shorts"
172
- try:
173
- response_2 = await client.get(search_url_2)
174
- response_2.raise_for_status()
175
- shorts_from_hashtag = await _extract_videos_from_html(response_2.text)
176
- print(f"DEBUG: Found {len(shorts_from_hashtag)} potential shorts from Hashtag Search.") # Debugging count
177
- for short in shorts_from_hashtag:
178
- if short and short.get('id') and short['id'] not in seen_video_ids:
179
- all_shorts_combined.append(short)
180
- seen_video_ids.add(short['id'])
181
- except (httpx.HTTPStatusError, httpx.RequestError, Exception) as e:
182
- print(f"ERROR: Error scraping from hashtag query ({search_url_2}): {e}")
183
- else:
184
- print(f"DEBUG: Hashtag query '{query_hashtag}' is empty after cleaning, skipping hashtag search.")
185
-
186
- print(f"\n--- Total unique shorts found: {len(all_shorts_combined)} ---") # Final count
187
- return all_shorts_combined
 
1
+ import os
 
 
 
2
  import re
3
+ from fastapi import FastAPI, Body, HTTPException, Query
4
+ from googleapiclient.discovery import build
5
+ from dotenv import load_dotenv
6
+ from typing import List, Dict, Any, Optional
7
+
8
+ # Load environment variables from .env file
9
+ load_dotenv()
10
 
11
  app = FastAPI()
12
 
13
+ # YouTube Data API Key
14
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
15
+
16
+ def find_relevant_youtube_shorts(movie_title: str, order: str = "relevance", max_results: int = 10) -> List[Dict[str, Any]]:
17
  """
18
+ Finds relevant YouTube Shorts for a given movie title using YouTube Data API v3.
19
+
20
+ Args:
21
+ movie_title (str): The title of the movie or TV series.
22
+ order (str): The order to sort the search results. Can be "relevance", "viewCount", or "rating".
23
+ max_results (int): The maximum number of shorts to return.
24
+
25
+ Returns:
26
+ list: A list of dictionaries, each representing a found YouTube Short
27
+ with its title, link, video_id, channel, and published_at.
28
  """
29
+ if not GOOGLE_API_KEY:
30
+ return []
31
+
32
+ youtube = build("youtube", "v3", developerKey=GOOGLE_API_KEY)
33
+
34
+ # Refined search query: More focused on movie title and direct shorts indicators.
35
+ # We'll rely more on post-filtering for excluding long-form.
36
+ # The query now explicitly requests "#shorts" or "YouTube Shorts" alongside the movie title.
37
+ search_query = f'"{movie_title}" #shorts OR "{movie_title}" "YouTube Shorts"'
38
+
39
+ try:
40
+ request = youtube.search().list(
41
+ q=search_query,
42
+ part="snippet",
43
+ type="video",
44
+ videoDuration="short", # Filters videos under 4 minutes
45
+ order=order,
46
+ maxResults=max_results * 5, # Fetch more to allow for aggressive filtering
47
+ )
48
+ response = request.execute()
49
+
50
+ potential_shorts = []
51
+ for item in response.get('items', []):
52
+ if item['id']['kind'] == 'youtube#video':
53
+ video_id = item['id']['videoId']
54
+ snippet = item['snippet']
55
+
56
+ potential_shorts.append({
57
+ "title": snippet['title'],
58
+ "link": f"https://www.youtube.com/watch?v={video_id}",
59
+ "video_id": video_id,
60
+ "channel_title": snippet['channelTitle'],
61
+ "published_at": snippet['publishedAt'],
62
+ "description": snippet['description'],
63
+ "thumbnails": snippet['thumbnails']['default']['url'],
64
+ })
65
+
66
+ # Stronger post-API filtering
67
+ final_shorts = []
68
+ # Indicators that strongly suggest long-form content
69
+ long_form_indicators = [
70
+ "full movie", "full episode", "season", "documentary",
71
+ "trailer review", "official trailer", "compilation", "movie review",
72
+ "best scenes", "explained", "summary", "recap", "analysis", "episodes",
73
+ "movie in 10 minutes", "full story", "watch full"
74
+ ]
75
+ # Strong indicators that it is a short video
76
+ shorts_indicators = ["#shorts", "youtube shorts", "short video", "short clip", "vertical video"]
77
+
78
+ movie_title_lower = movie_title.lower()
79
+
80
+ for short in potential_shorts:
81
+ title_lower = short['title'].lower()
82
+ description_lower = short['description'].lower()
83
 
84
+ # Rule 1: Must contain a strong shorts indicator
85
+ has_strong_shorts_indicator = any(indicator in title_lower or indicator in description_lower for indicator in shorts_indicators)
86
+
87
+ # Rule 2: Ensure movie title (or part of it) is present in title/description for strong relevance
88
+ # Using split for multi-word titles to check for partial matches too
89
+ is_relevant_to_movie = movie_title_lower in title_lower or movie_title_lower in description_lower or \
90
+ any(word in title_lower or word in description_lower for word in movie_title_lower.split())
91
 
92
 
93
+ # Rule 3: Must NOT contain strong long-form indicators
94
+ is_not_long_form = not any(indicator in title_lower or indicator in description_lower for indicator in long_form_indicators)
95
+
96
+ if has_strong_shorts_indicator and is_relevant_to_movie and is_not_long_form:
97
+ final_shorts.append(short)
98
+
99
+ if len(final_shorts) >= max_results:
100
  break
 
 
 
 
 
101
 
102
+ return final_shorts
 
 
103
 
104
+ except Exception as e:
105
+ print(f"An error occurred during YouTube Data API search for '{movie_title}': {e}")
106
+ return []
107
 
108
+ @app.get("/get_shorts_for_movie")
109
+ async def get_shorts_for_movie(
110
+ movie_title: str = Query(..., description="Title of the movie or TV series"),
111
+ search_order: Optional[str] = Query("relevance", description="Order by relevance, viewCount, or rating"),
112
+ max_shorts: Optional[int] = Query(10, description="Maximum number of shorts to return")
113
+ ):
114
+ if not movie_title:
115
+ raise HTTPException(status_code=400, detail="'movie_title' query parameter is required.")
116
+
117
+ if not GOOGLE_API_KEY:
118
+ raise HTTPException(status_code=500, detail="Google API Key is not configured.")
119
+
120
+ relevant_shorts = find_relevant_youtube_shorts(movie_title, order=search_order, max_results=max_shorts)
121
+
122
+ return {"movie_title": movie_title, "related_shorts": relevant_shorts}