UrlCategorise 0.1.7 → 0.1.11

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.
@@ -7,31 +7,40 @@ require 'json'
7
7
  require 'fileutils'
8
8
  require 'time'
9
9
  require 'set'
10
+ require 'optparse'
11
+ require 'thread'
10
12
 
11
13
  module VideoListGenerator
12
14
  class ExtractorParser
13
15
  YT_DLP_API_URL = 'https://api.github.com/repos/yt-dlp/yt-dlp/contents/yt_dlp/extractor'
14
-
15
- def initialize
16
+
17
+ attr_accessor :max_download_workers, :max_process_workers, :parallel_enabled
18
+
19
+ def initialize(options = {})
16
20
  @video_domains = Set.new
17
21
  @video_regexes = []
22
+ @domains_mutex = Mutex.new
23
+ @regexes_mutex = Mutex.new
24
+ @max_download_workers = options[:max_download_workers] || 16
25
+ @max_process_workers = options[:max_process_workers] || 8
26
+ @parallel_enabled = options.fetch(:parallel_enabled, true)
18
27
  end
19
28
 
20
29
  def generate_lists
21
- puts "🎬 Generating video hosting lists from yt-dlp extractors..."
22
-
23
- puts "📝 Adding manually curated video hosting domains..."
30
+ puts '🎬 Generating video hosting lists from yt-dlp extractors...'
31
+
32
+ puts '📝 Adding manually curated video hosting domains...'
24
33
  add_manual_domains
25
-
26
- puts "📡 Fetching yt-dlp extractor data..."
34
+
35
+ puts '📡 Fetching yt-dlp extractor data...'
27
36
  fetch_extractor_data
28
-
37
+
29
38
  create_lists_directory
30
39
  generate_hosts_file
31
40
  generate_regex_file
32
-
33
- puts "✅ Video hosting lists generated successfully!"
34
- puts "📁 Files created in lists/ directory:"
41
+
42
+ puts '✅ Video hosting lists generated successfully!'
43
+ puts '📁 Files created in lists/ directory:'
35
44
  puts " - video_hosting_domains.hosts (#{@video_domains.size} domains)"
36
45
  puts " - video_url_patterns.txt (#{@video_regexes.size} patterns)"
37
46
  end
@@ -55,177 +64,310 @@ module VideoListGenerator
55
64
  'streamable.com',
56
65
  'wistia.com', 'fast.wistia.com', 'fast.wistia.net'
57
66
  ]
58
-
67
+
59
68
  core_domains.each { |domain| @video_domains.add(domain) }
60
69
  end
61
70
 
62
71
  def fetch_extractor_data
63
72
  uri = URI(YT_DLP_API_URL)
64
73
  response = Net::HTTP.get_response(uri)
65
-
74
+
66
75
  unless response.code == '200'
67
- puts "⚠️ Could not fetch from yt-dlp API, using manual domains only"
76
+ puts '⚠️ Could not fetch from yt-dlp API, using manual domains only'
68
77
  return
69
78
  end
70
-
79
+
71
80
  files = JSON.parse(response.body)
72
-
81
+
73
82
  # Process extractor files to find domains and patterns
74
83
  extractor_files = files.select { |file| file['name'].end_with?('.py') && file['name'] != '__init__.py' }
75
-
84
+
76
85
  puts "📝 Processing #{extractor_files.length} extractor files..."
77
-
78
- extractor_files.each_with_index do |file, index|
79
- print "\r\e[K" # Clear the entire line
80
- print "Processing #{index + 1}/#{extractor_files.length}: #{file['name']}"
81
- process_extractor_file(file)
86
+
87
+ puts "⬇️ Downloading #{extractor_files.length} files with #{@max_download_workers} threads..."
88
+ file_contents = download_files_parallel(extractor_files)
89
+
90
+ if @parallel_enabled
91
+ puts "🚀 Processing #{file_contents.length} files with #{@max_process_workers} threads..."
92
+ process_content_parallel(file_contents)
93
+ else
94
+ puts "📥 Processing #{file_contents.length} files sequentially..."
95
+ process_content_sequential(file_contents)
82
96
  end
83
-
97
+
84
98
  puts "\n🎯 Found #{@video_domains.size} unique video hosting domains"
85
99
  puts "🔍 Extracted #{@video_regexes.size} URL pattern regexes"
86
100
  end
87
101
 
88
- def process_extractor_file(file_info)
89
- begin
90
- response = Net::HTTP.get_response(URI(file_info['download_url']))
91
- return unless response.code == '200'
92
-
93
- content = response.body
94
-
95
- extract_domains_from_content(content, file_info['name'])
96
- extract_regexes_from_content(content, file_info['name'])
97
- rescue StandardError => e
98
- # Silently skip errors to avoid spam
102
+ def download_files_parallel(extractor_files)
103
+ file_contents = []
104
+ mutex = Mutex.new
105
+ download_count = 0
106
+ total = extractor_files.length
107
+
108
+ queue = Queue.new
109
+ extractor_files.each { |f| queue << f }
110
+
111
+ workers = @max_download_workers.times.map do
112
+ Thread.new do
113
+ while (file = begin; queue.pop(true); rescue ThreadError; nil; end)
114
+ begin
115
+ response = Net::HTTP.get_response(URI(file['download_url']))
116
+ if response.code == '200'
117
+ mutex.synchronize do
118
+ file_contents << { name: file['name'], content: response.body }
119
+ download_count += 1
120
+ print "\r\e[K"
121
+ print "Downloaded #{download_count}/#{total} files"
122
+ end
123
+ else
124
+ mutex.synchronize { download_count += 1 }
125
+ end
126
+ rescue StandardError
127
+ mutex.synchronize { download_count += 1 }
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ workers.each(&:join)
134
+
135
+ puts "\n✅ Downloaded #{file_contents.length}/#{total} files successfully"
136
+ file_contents
137
+ end
138
+
139
+ def process_content_sequential(file_contents)
140
+ file_contents.each_with_index do |file_data, index|
141
+ print "\r\e[K"
142
+ print "Processing #{index + 1}/#{file_contents.length}: #{file_data[:name]}"
143
+ extract_domains_from_content(file_data[:content], file_data[:name])
144
+ extract_regexes_from_content(file_data[:content], file_data[:name])
145
+ end
146
+ end
147
+
148
+ def process_content_parallel(file_contents)
149
+ queue = Queue.new
150
+ file_contents.each { |f| queue << f }
151
+ process_count = 0
152
+ total = file_contents.length
153
+ count_mutex = Mutex.new
154
+
155
+ workers = @max_process_workers.times.map do
156
+ Thread.new do
157
+ while (file_data = begin; queue.pop(true); rescue ThreadError; nil; end)
158
+ # Each thread collects its own results to minimize lock contention
159
+ local_domains = Set.new
160
+ local_regexes = []
161
+
162
+ process_file_content(file_data[:content], file_data[:name], local_domains, local_regexes)
163
+
164
+ # Merge results under lock
165
+ @domains_mutex.synchronize { local_domains.each { |d| @video_domains.add(d) } }
166
+ @regexes_mutex.synchronize { @video_regexes.concat(local_regexes) }
167
+
168
+ count_mutex.synchronize do
169
+ process_count += 1
170
+ print "\r\e[K"
171
+ print "Processed #{process_count}/#{total} files"
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ workers.each(&:join)
178
+ end
179
+
180
+ def process_file_content(content, filename, domains, regexes)
181
+ # Look for _VALID_URL patterns and extract domains
182
+ valid_url_patterns = content.scan(/_VALID_URL\s*=\s*r?['"]([^'"]+)['"]/m)
183
+
184
+ valid_url_patterns.each do |pattern|
185
+ pattern = pattern[0] if pattern.is_a?(Array)
186
+ extracted = extract_domains_from_regex(pattern)
187
+ extracted.each { |domain| domains.add(domain) }
188
+
189
+ cleaned_pattern = clean_regex_pattern(pattern)
190
+ if cleaned_pattern && !cleaned_pattern.empty? && is_useful_pattern?(cleaned_pattern)
191
+ regexes << {
192
+ source: filename.gsub('.py', ''),
193
+ pattern: cleaned_pattern,
194
+ original: pattern
195
+ }
196
+ end
197
+ end
198
+
199
+ # Look for IE_NAME patterns
200
+ ie_name_patterns = content.scan(/IE_NAME\s*=\s*['"]([^'"]+)['"]/m)
201
+ ie_name_patterns.each do |name|
202
+ name = name[0] if name.is_a?(Array)
203
+ next unless name && name.include?('.') && valid_domain?(name)
204
+
205
+ domains.add(name.downcase)
206
+ end
207
+
208
+ # Look for hardcoded URLs in the content
209
+ url_patterns = content.scan(%r{['"]https?://(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})}i)
210
+ url_patterns.each do |match|
211
+ domain = match[0] if match.is_a?(Array)
212
+ next unless domain && valid_domain?(domain.downcase)
213
+
214
+ domains.add(domain.downcase)
99
215
  end
216
+
217
+ # Look for class-based domain hints from filename
218
+ return unless filename.end_with?('.py')
219
+
220
+ base_name = filename.gsub('.py', '').downcase
221
+ if base_name.include?('.')
222
+ potential_domain = base_name
223
+ elsif base_name.length > 3 && !%w[common generic base].include?(base_name)
224
+ potential_domain = "#{base_name}.com"
225
+ end
226
+
227
+ return unless potential_domain && valid_domain?(potential_domain)
228
+
229
+ domains.add(potential_domain)
230
+ end
231
+
232
+ def process_extractor_file(file_info)
233
+ response = Net::HTTP.get_response(URI(file_info['download_url']))
234
+ return unless response.code == '200'
235
+
236
+ content = response.body
237
+
238
+ extract_domains_from_content(content, file_info['name'])
239
+ extract_regexes_from_content(content, file_info['name'])
240
+ rescue StandardError
241
+ # Silently skip errors to avoid spam
100
242
  end
101
243
 
102
244
  def extract_domains_from_content(content, filename)
103
245
  # Look for _VALID_URL patterns and extract domains
104
246
  valid_url_patterns = content.scan(/_VALID_URL\s*=\s*r?['"]([^'"]+)['"]/m)
105
-
247
+
106
248
  valid_url_patterns.each do |pattern|
107
249
  pattern = pattern[0] if pattern.is_a?(Array)
108
250
  domains = extract_domains_from_regex(pattern)
109
251
  domains.each { |domain| @video_domains.add(domain) }
110
252
  end
111
-
253
+
112
254
  # Look for IE_NAME patterns
113
255
  ie_name_patterns = content.scan(/IE_NAME\s*=\s*['"]([^'"]+)['"]/m)
114
256
  ie_name_patterns.each do |name|
115
257
  name = name[0] if name.is_a?(Array)
116
258
  next unless name && name.include?('.') && valid_domain?(name)
259
+
117
260
  @video_domains.add(name.downcase)
118
261
  end
119
-
262
+
120
263
  # Look for hardcoded URLs in the content
121
- url_patterns = content.scan(/['"]https?:\/\/(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i)
264
+ url_patterns = content.scan(%r{['"]https?://(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})}i)
122
265
  url_patterns.each do |match|
123
266
  domain = match[0] if match.is_a?(Array)
124
267
  next unless domain && valid_domain?(domain.downcase)
268
+
125
269
  @video_domains.add(domain.downcase)
126
270
  end
127
-
271
+
128
272
  # Look for class-based domain hints from filename
129
- if filename.end_with?('.py')
130
- base_name = filename.gsub('.py', '').downcase
131
- # Try to infer domain from extractor name
132
- if base_name.include?('.')
133
- potential_domain = base_name
134
- elsif base_name.length > 3 && !%w[common generic base].include?(base_name)
135
- # Common patterns: youtube -> youtube.com, vimeo -> vimeo.com
136
- potential_domain = "#{base_name}.com"
137
- end
138
-
139
- if potential_domain && valid_domain?(potential_domain)
140
- @video_domains.add(potential_domain)
141
- end
273
+ return unless filename.end_with?('.py')
274
+
275
+ base_name = filename.gsub('.py', '').downcase
276
+ # Try to infer domain from extractor name
277
+ if base_name.include?('.')
278
+ potential_domain = base_name
279
+ elsif base_name.length > 3 && !%w[common generic base].include?(base_name)
280
+ # Common patterns: youtube -> youtube.com, vimeo -> vimeo.com
281
+ potential_domain = "#{base_name}.com"
142
282
  end
283
+
284
+ return unless potential_domain && valid_domain?(potential_domain)
285
+
286
+ @video_domains.add(potential_domain)
143
287
  end
144
288
 
145
289
  def extract_regexes_from_content(content, filename)
146
290
  # Extract _VALID_URL patterns for video detection
147
291
  valid_url_patterns = content.scan(/_VALID_URL\s*=\s*r?['"]([^'"]+)['"]/m)
148
-
292
+
149
293
  valid_url_patterns.each do |pattern|
150
294
  pattern = pattern[0] if pattern.is_a?(Array)
151
-
295
+
152
296
  cleaned_pattern = clean_regex_pattern(pattern)
153
- if cleaned_pattern && !cleaned_pattern.empty? && is_useful_pattern?(cleaned_pattern)
154
- @video_regexes << {
155
- source: filename.gsub('.py', ''),
156
- pattern: cleaned_pattern,
157
- original: pattern
158
- }
159
- end
297
+ next unless cleaned_pattern && !cleaned_pattern.empty? && is_useful_pattern?(cleaned_pattern)
298
+
299
+ @video_regexes << {
300
+ source: filename.gsub('.py', ''),
301
+ pattern: cleaned_pattern,
302
+ original: pattern
303
+ }
160
304
  end
161
305
  end
162
306
 
163
307
  def extract_domains_from_regex(pattern)
164
308
  domains = Set.new
165
-
309
+
166
310
  # Extract domains from common regex patterns - more comprehensive
167
311
  patterns_to_try = [
168
312
  # Standard URLs
169
- /https?:\/\/(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i,
313
+ %r{https?://(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})}i,
170
314
  # Escaped URLs with backslashes
171
- /https?:\\\/\\\/(?:www\\.)?([a-zA-Z0-9-]+\\.[a-zA-Z]{2,})/i,
315
+ %r{https?:\\/\\/(?:www\\.)?([a-zA-Z0-9-]+\\.[a-zA-Z]{2,})}i,
172
316
  # Domain patterns with optional www (escaped)
173
- /\(\?\:www\\\.\)\?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i,
317
+ /\(\?:www\\\.\)\?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i,
174
318
  # Simple domain references
175
319
  /([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i
176
320
  ]
177
-
321
+
178
322
  patterns_to_try.each do |regex|
179
323
  matches = pattern.scan(regex)
180
324
  matches.each do |match|
181
325
  domain = match.is_a?(Array) ? match[0] : match
182
326
  domain = clean_domain(domain) if domain
183
- if domain && valid_domain?(domain.downcase)
184
- domains.add(domain.downcase)
185
- end
327
+ domains.add(domain.downcase) if domain && valid_domain?(domain.downcase)
186
328
  end
187
329
  end
188
-
330
+
189
331
  domains.to_a
190
332
  end
191
333
 
192
334
  def clean_domain(domain)
193
335
  return nil unless domain
194
-
336
+
195
337
  # Remove regex escapes and clean up
196
- cleaned = domain.gsub(/\\\./, '.').gsub(/[\(\)\?\:\|\\]/, '')
338
+ cleaned = domain.gsub(/\\\./, '.').gsub(/[()?:|\\]/, '')
197
339
  cleaned = cleaned.strip
198
-
340
+
199
341
  # Remove trailing regex syntax
200
- cleaned = cleaned.split(/[\(\)\[\]]/)[0] if cleaned.include?('(') || cleaned.include?('[')
201
-
342
+ cleaned = cleaned.split(/[()\[\]]/)[0] if cleaned.include?('(') || cleaned.include?('[')
343
+
202
344
  cleaned
203
345
  end
204
346
 
205
347
  def clean_regex_pattern(pattern)
206
348
  return nil unless pattern
207
-
349
+
208
350
  # Simplify common patterns for practical use
209
351
  cleaned = pattern.dup
210
-
352
+
211
353
  # Remove complex lookaheads/lookbehinds
212
- cleaned = cleaned.gsub(/\(\?\![^)]*\)/, '')
213
- cleaned = cleaned.gsub(/\(\?\<\![^)]*\)/, '')
214
-
354
+ cleaned = cleaned.gsub(/\(\?![^)]*\)/, '')
355
+ cleaned = cleaned.gsub(/\(\?<![^)]*\)/, '')
356
+
215
357
  # Simplify common patterns
216
- cleaned = cleaned.gsub(/\(\?\:www\\\.\)\?/, '(?:www\.)?')
358
+ cleaned = cleaned.gsub(/\(\?:www\\\.\)\?/, '(?:www\.)?')
217
359
  cleaned = cleaned.gsub(/\\\./, '\.')
218
-
360
+
219
361
  # Validate regex with warning suppression
220
362
  begin
221
363
  original_warning = $-w
222
- $-w = nil # Disable warnings
364
+ $-w = nil # Disable warnings
223
365
  Regexp.new(cleaned)
224
366
  cleaned
225
367
  rescue RegexpError
226
368
  nil
227
369
  ensure
228
- $-w = original_warning # Restore warnings
370
+ $-w = original_warning # Restore warnings
229
371
  end
230
372
  end
231
373
 
@@ -235,7 +377,7 @@ module VideoListGenerator
235
377
  return false if pattern.length < 10
236
378
  return false if pattern.include?('blob:')
237
379
  return false if pattern.match?(/\$\s*$/) # ends with just $
238
-
380
+
239
381
  true
240
382
  end
241
383
 
@@ -246,6 +388,7 @@ module VideoListGenerator
246
388
  return false if domain.include?('example.') || domain.include?('test.')
247
389
  return false if domain.include?('localhost')
248
390
  return false if domain.end_with?('.html') || domain.end_with?('.htm')
391
+
249
392
  true
250
393
  end
251
394
 
@@ -255,10 +398,10 @@ module VideoListGenerator
255
398
 
256
399
  def generate_hosts_file
257
400
  hosts_file_path = 'lists/video_hosting_domains.hosts'
258
-
401
+
259
402
  File.open(hosts_file_path, 'w') do |file|
260
403
  write_hosts_header(file)
261
-
404
+
262
405
  @video_domains.sort.each do |domain|
263
406
  file.puts "0.0.0.0 #{domain}"
264
407
  file.puts "0.0.0.0 www.#{domain}" unless domain.start_with?('www.')
@@ -268,34 +411,34 @@ module VideoListGenerator
268
411
 
269
412
  def generate_regex_file
270
413
  regex_file_path = 'lists/video_url_patterns.txt'
271
-
414
+
272
415
  File.open(regex_file_path, 'w') do |file|
273
416
  write_regex_header(file)
274
-
417
+
275
418
  # Add manual high-priority patterns first
276
419
  add_manual_video_patterns(file)
277
-
420
+
278
421
  @video_regexes.each do |regex_info|
279
422
  file.puts "# Source: #{regex_info[:source]}"
280
423
  file.puts "# Pattern: #{regex_info[:pattern]}"
281
424
  file.puts "# Original: #{regex_info[:original]}"
282
425
  file.puts regex_info[:pattern]
283
- file.puts ""
426
+ file.puts ''
284
427
  end
285
428
  end
286
429
  end
287
430
 
288
431
  def write_hosts_header(file)
289
- file.puts "# Video Hosting Domains - PiHole Compatible Hosts File"
432
+ file.puts '# Video Hosting Domains - PiHole Compatible Hosts File'
290
433
  file.puts "# Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S UTC')}"
291
- file.puts "# Source: yt-dlp extractors (https://github.com/yt-dlp/yt-dlp)"
292
- file.puts "# Purpose: Block access to video hosting websites"
293
- file.puts "# Format: 0.0.0.0 domain.com"
294
- file.puts "#"
295
- file.puts "# This file contains domains extracted from yt-dlp video extractors"
296
- file.puts "# to help identify and categorize video hosting websites."
297
- file.puts "#"
298
- file.puts ""
434
+ file.puts '# Source: yt-dlp extractors (https://github.com/yt-dlp/yt-dlp)'
435
+ file.puts '# Purpose: Block access to video hosting websites'
436
+ file.puts '# Format: 0.0.0.0 domain.com'
437
+ file.puts '#'
438
+ file.puts '# This file contains domains extracted from yt-dlp video extractors'
439
+ file.puts '# to help identify and categorize video hosting websites.'
440
+ file.puts '#'
441
+ file.puts ''
299
442
  end
300
443
 
301
444
  def add_manual_video_patterns(file)
@@ -303,12 +446,12 @@ module VideoListGenerator
303
446
  manual_patterns = [
304
447
  {
305
448
  source: 'manual_youtube',
306
- pattern: 'https?://(?:www\\.)?(?:youtube\\.com/watch\\?v=|youtu\\.be/)[a-zA-Z0-9_-]{11}',
449
+ pattern: 'https?://(?:www\\.)?(?:youtube\\.com/watch\\?.*[&?]?v=|youtu\\.be/)[a-zA-Z0-9_-]{11}',
307
450
  description: 'YouTube video watch URLs'
308
451
  },
309
452
  {
310
453
  source: 'manual_youtube_shorts',
311
- pattern: 'https?://(?:www\\.)?youtube\\.com/shorts/[a-zA-Z0-9_-]{11}',
454
+ pattern: 'https?://(?:www\\.)?youtube\\.com/shorts/[a-zA-Z0-9_-]{11}(?:\\?.*)?$',
312
455
  description: 'YouTube Shorts URLs'
313
456
  },
314
457
  {
@@ -332,42 +475,78 @@ module VideoListGenerator
332
475
  description: 'TikTok video URLs'
333
476
  }
334
477
  ]
335
-
336
- file.puts "# ===== MANUAL HIGH-PRIORITY PATTERNS ====="
337
- file.puts ""
338
-
478
+
479
+ file.puts '# ===== MANUAL HIGH-PRIORITY PATTERNS ====='
480
+ file.puts ''
481
+
339
482
  manual_patterns.each do |pattern_info|
340
483
  file.puts "# Source: #{pattern_info[:source]}"
341
484
  file.puts "# Description: #{pattern_info[:description]}"
342
485
  file.puts "# Pattern: #{pattern_info[:pattern]}"
343
486
  file.puts pattern_info[:pattern]
344
- file.puts ""
487
+ file.puts ''
345
488
  end
346
-
347
- file.puts "# ===== EXTRACTED PATTERNS FROM YT-DLP ====="
348
- file.puts ""
489
+
490
+ file.puts '# ===== EXTRACTED PATTERNS FROM YT-DLP ====='
491
+ file.puts ''
349
492
  end
350
493
 
351
494
  def write_regex_header(file)
352
- file.puts "# Video URL Detection Patterns"
495
+ file.puts '# Video URL Detection Patterns'
353
496
  file.puts "# Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S UTC')}"
354
- file.puts "# Source: yt-dlp extractors (https://github.com/yt-dlp/yt-dlp) + manual patterns"
355
- file.puts "# Purpose: Regex patterns to detect video URLs vs other content"
356
- file.puts "#"
357
- file.puts "# These patterns help distinguish between:"
358
- file.puts "# - Direct video content URLs"
359
- file.puts "# - Homepage, playlist, user profile, community content URLs"
360
- file.puts "#"
361
- file.puts "# Usage: Use these patterns to categorize URLs from video hosting domains"
362
- file.puts "# to determine if they contain actual video content or other resources"
363
- file.puts "#"
364
- file.puts ""
497
+ file.puts '# Source: yt-dlp extractors (https://github.com/yt-dlp/yt-dlp) + manual patterns'
498
+ file.puts '# Purpose: Regex patterns to detect video URLs vs other content'
499
+ file.puts '#'
500
+ file.puts '# These patterns help distinguish between:'
501
+ file.puts '# - Direct video content URLs'
502
+ file.puts '# - Homepage, playlist, user profile, community content URLs'
503
+ file.puts '#'
504
+ file.puts '# Usage: Use these patterns to categorize URLs from video hosting domains'
505
+ file.puts '# to determine if they contain actual video content or other resources'
506
+ file.puts '#'
507
+ file.puts ''
365
508
  end
366
509
  end
367
510
  end
368
511
 
369
512
  # Run the generator if this file is executed directly
370
513
  if __FILE__ == $0
371
- generator = VideoListGenerator::ExtractorParser.new
514
+ # Default configuration
515
+ options = {
516
+ max_download_workers: 16,
517
+ max_process_workers: 8,
518
+ verbose: false
519
+ }
520
+
521
+ # Parse command-line options
522
+ OptionParser.new do |opts|
523
+ opts.banner = "Usage: #{$0} [options]"
524
+ opts.separator ''
525
+ opts.separator 'Generate video hosting domain lists and URL patterns from yt-dlp extractors'
526
+ opts.separator ''
527
+
528
+ opts.on('-d', '--download-workers NUM', Integer, "Number of download threads (default: #{options[:max_download_workers]})") do |num|
529
+ options[:max_download_workers] = num
530
+ end
531
+
532
+ opts.on('-p', '--process-workers NUM', Integer, "Number of processing threads (default: #{options[:max_process_workers]})") do |num|
533
+ options[:max_process_workers] = num
534
+ end
535
+
536
+ opts.on('-v', '--verbose', 'Verbose output') do
537
+ options[:verbose] = true
538
+ end
539
+
540
+ opts.on('--no-parallel', 'Disable parallel processing (use sequential only)') do
541
+ options[:parallel_enabled] = false
542
+ end
543
+
544
+ opts.on('-h', '--help', 'Show this help message') do
545
+ puts opts
546
+ exit
547
+ end
548
+ end.parse!
549
+
550
+ generator = VideoListGenerator::ExtractorParser.new(options)
372
551
  generator.generate_lists
373
- end
552
+ end