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 +1 -1
- package/requirements.txt +1 -3
- package/tools/downloaders/youtube_search.py +125 -54
package/package.json
CHANGED
package/requirements.txt
CHANGED
|
@@ -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
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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':
|
|
111
|
-
'views': video_info.get('
|
|
112
|
-
'upload_date': video_info.get('
|
|
113
|
-
'channel': video_info.get('channel'
|
|
114
|
-
'description': video_info.get('description'
|
|
115
|
-
'is_live': video_info.get('
|
|
116
|
-
'category': video_info.get('
|
|
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:
|