videonut 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "videonut",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "AI-powered YouTube documentary production pipeline with 10 specialized agents for research, scripting, and asset management",
5
5
  "keywords": [
6
6
  "youtube",
package/requirements.txt CHANGED
@@ -4,6 +4,4 @@ playwright
4
4
  requests
5
5
  beautifulsoup4
6
6
  pypdf
7
- youtube-transcript-api
8
- youtube-search-python
9
- httpx
7
+ youtube-transcript-api
@@ -2,24 +2,19 @@
2
2
  """
3
3
  YouTube Search Tool for VideoNut
4
4
  Searches YouTube for videos matching a query and returns structured results.
5
- Uses youtube-search-python library for searching without API key.
5
+ Uses yt-dlp for reliable, actively maintained YouTube searching.
6
6
  """
7
7
 
8
8
  import sys
9
9
  import argparse
10
10
  import json
11
+ import subprocess
12
+ import re
11
13
  from datetime import datetime
12
14
 
13
- try:
14
- from youtubesearchpython import VideosSearch, Video
15
- except ImportError:
16
- print("Error: youtube-search-python not installed. Install with: pip install youtube-search-python")
17
- sys.exit(1)
18
-
19
-
20
15
  def search_youtube(query, max_results=10, filter_year=None):
21
16
  """
22
- Search YouTube for videos matching the query.
17
+ Search YouTube for videos matching the query using yt-dlp.
23
18
 
24
19
  Args:
25
20
  query: Search query string
@@ -30,47 +25,102 @@ def search_youtube(query, max_results=10, filter_year=None):
30
25
  List of video dictionaries with title, url, duration, views, upload_date, channel
31
26
  """
32
27
  try:
33
- videos_search = VideosSearch(query, limit=max_results * 2) # Get extra to filter
34
- results = videos_search.result()
28
+ # Use yt-dlp to search YouTube
29
+ search_query = f"ytsearch{max_results * 2}:{query}" # Get extra to filter
30
+
31
+ cmd = [
32
+ "yt-dlp",
33
+ "--flat-playlist",
34
+ "--dump-json",
35
+ "--no-warnings",
36
+ "--ignore-errors",
37
+ search_query
38
+ ]
39
+
40
+ result = subprocess.run(
41
+ cmd,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=60
45
+ )
46
+
47
+ if result.returncode != 0 and not result.stdout:
48
+ print(f"Error: yt-dlp search failed", file=sys.stderr)
49
+ return []
35
50
 
36
51
  videos = []
37
- for video in results.get('result', []):
38
- video_data = {
39
- 'title': video.get('title', 'Unknown'),
40
- 'url': video.get('link', ''),
41
- 'video_id': video.get('id', ''),
42
- 'duration': video.get('duration', 'Unknown'),
43
- 'views': video.get('viewCount', {}).get('text', 'Unknown'),
44
- 'upload_date': video.get('publishedTime', 'Unknown'),
45
- 'channel': video.get('channel', {}).get('name', 'Unknown'),
46
- 'description': video.get('descriptionSnippet', [{}])[0].get('text', '') if video.get('descriptionSnippet') else '',
47
- 'thumbnail': video.get('thumbnails', [{}])[0].get('url', '') if video.get('thumbnails') else ''
48
- }
49
-
50
- # Filter by year if specified
51
- if filter_year:
52
- upload_text = video_data['upload_date'].lower()
53
- # Check if it contains year info
54
- if str(filter_year) in upload_text or f"{filter_year}" in video_data['title']:
55
- videos.append(video_data)
56
- elif 'year' in upload_text:
57
- # Try to parse "X years ago"
58
- try:
59
- years_ago = int(upload_text.split()[0])
60
- current_year = datetime.now().year
61
- video_year = current_year - years_ago
62
- if video_year <= filter_year:
63
- videos.append(video_data)
64
- except:
65
- pass
66
- else:
52
+ for line in result.stdout.strip().split('\n'):
53
+ if not line:
54
+ continue
55
+ try:
56
+ video = json.loads(line)
57
+
58
+ # Extract duration - yt-dlp provides it in seconds
59
+ duration_secs = video.get('duration')
60
+ if duration_secs:
61
+ mins, secs = divmod(int(duration_secs), 60)
62
+ hours, mins = divmod(mins, 60)
63
+ if hours > 0:
64
+ duration_str = f"{hours}:{mins:02d}:{secs:02d}"
65
+ else:
66
+ duration_str = f"{mins}:{secs:02d}"
67
+ else:
68
+ duration_str = "Unknown"
69
+
70
+ # Format view count
71
+ view_count = video.get('view_count')
72
+ if view_count:
73
+ if view_count >= 1000000:
74
+ views_str = f"{view_count/1000000:.1f}M views"
75
+ elif view_count >= 1000:
76
+ views_str = f"{view_count/1000:.1f}K views"
77
+ else:
78
+ views_str = f"{view_count} views"
79
+ else:
80
+ views_str = "Unknown"
81
+
82
+ video_data = {
83
+ 'title': video.get('title', 'Unknown'),
84
+ 'url': video.get('url') or f"https://www.youtube.com/watch?v={video.get('id', '')}",
85
+ 'video_id': video.get('id', ''),
86
+ 'duration': duration_str,
87
+ 'duration_seconds': duration_secs,
88
+ 'views': views_str,
89
+ 'view_count': view_count,
90
+ 'upload_date': video.get('upload_date', 'Unknown'),
91
+ 'channel': video.get('channel') or video.get('uploader', 'Unknown'),
92
+ 'description': (video.get('description') or '')[:200],
93
+ 'thumbnail': video.get('thumbnail', '')
94
+ }
95
+
96
+ # Filter by year if specified
97
+ if filter_year:
98
+ upload_date = video_data['upload_date']
99
+ if upload_date and upload_date != 'Unknown':
100
+ # yt-dlp provides date as YYYYMMDD
101
+ try:
102
+ video_year = int(upload_date[:4])
103
+ if video_year != filter_year:
104
+ continue
105
+ except (ValueError, TypeError):
106
+ pass
107
+
67
108
  videos.append(video_data)
68
-
69
- if len(videos) >= max_results:
70
- break
109
+
110
+ if len(videos) >= max_results:
111
+ break
112
+
113
+ except json.JSONDecodeError:
114
+ continue
71
115
 
72
116
  return videos
73
117
 
118
+ except subprocess.TimeoutExpired:
119
+ print("Error: YouTube search timed out", file=sys.stderr)
120
+ return []
121
+ except FileNotFoundError:
122
+ print("Error: yt-dlp not found. Install with: pip install yt-dlp", file=sys.stderr)
123
+ return []
74
124
  except Exception as e:
75
125
  print(f"Error searching YouTube: {str(e)}", file=sys.stderr)
76
126
  return []
@@ -102,18 +152,39 @@ def format_results(videos, output_format='text'):
102
152
 
103
153
 
104
154
  def get_video_details(video_url):
105
- """Get detailed information about a specific video"""
155
+ """Get detailed information about a specific video using yt-dlp"""
106
156
  try:
107
- video_info = Video.getInfo(video_url)
157
+ cmd = [
158
+ "yt-dlp",
159
+ "--dump-json",
160
+ "--no-download",
161
+ "--no-warnings",
162
+ video_url
163
+ ]
164
+
165
+ result = subprocess.run(
166
+ cmd,
167
+ capture_output=True,
168
+ text=True,
169
+ timeout=30
170
+ )
171
+
172
+ if result.returncode != 0:
173
+ return None
174
+
175
+ video_info = json.loads(result.stdout)
176
+
177
+ duration_secs = video_info.get('duration', 0)
178
+
108
179
  return {
109
180
  'title': video_info.get('title', 'Unknown'),
110
- 'duration_seconds': video_info.get('duration', {}).get('secondsText', 'Unknown'),
111
- 'views': video_info.get('viewCount', {}).get('text', 'Unknown'),
112
- 'upload_date': video_info.get('publishDate', 'Unknown'),
113
- 'channel': video_info.get('channel', {}).get('name', 'Unknown'),
114
- 'description': video_info.get('description', '')[:500],
115
- 'is_live': video_info.get('isLiveNow', False),
116
- 'category': video_info.get('category', 'Unknown')
181
+ 'duration_seconds': duration_secs,
182
+ 'views': video_info.get('view_count', 'Unknown'),
183
+ 'upload_date': video_info.get('upload_date', 'Unknown'),
184
+ 'channel': video_info.get('channel') or video_info.get('uploader', 'Unknown'),
185
+ 'description': (video_info.get('description') or '')[:500],
186
+ 'is_live': video_info.get('is_live', False),
187
+ 'category': video_info.get('categories', ['Unknown'])[0] if video_info.get('categories') else 'Unknown'
117
188
  }
118
189
  except Exception as e:
119
190
  print(f"Error getting video details: {str(e)}", file=sys.stderr)
@@ -122,7 +193,7 @@ def get_video_details(video_url):
122
193
 
123
194
  def main():
124
195
  parser = argparse.ArgumentParser(
125
- description="Search YouTube for videos. Returns video titles, URLs, and metadata.",
196
+ description="Search YouTube for videos using yt-dlp. Returns video titles, URLs, and metadata.",
126
197
  formatter_class=argparse.RawDescriptionHelpFormatter,
127
198
  epilog="""
128
199
  Examples: