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.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +9 -2
- data/.gitignore +1 -0
- data/.rubocop.yml +56 -0
- data/.sublime-project +14 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +89 -41
- data/README.md +167 -9
- data/Rakefile +1 -1
- data/bin/check_lists +124 -22
- data/bin/export_csv +45 -36
- data/bin/export_hosts +34 -24
- data/bin/generate_categorised_lists +533 -0
- data/bin/generate_social_media_lists +398 -0
- data/bin/generate_video_lists +303 -124
- data/bin/generate_video_lists_focused +381 -0
- data/correct_usage_example.rb +18 -18
- data/lib/url_categorise/active_record_client.rb +11 -11
- data/lib/url_categorise/client.rb +856 -226
- data/lib/url_categorise/constants.rb +148 -70
- data/lib/url_categorise/dataset_processor.rb +68 -64
- data/lib/url_categorise/iab_compliance.rb +111 -99
- data/lib/url_categorise/models.rb +15 -15
- data/lib/url_categorise/version.rb +1 -1
- data/lib/url_categorise.rb +14 -14
- data/lists/blogging_domains.hosts +62 -0
- data/lists/crypto_domains.hosts +46 -0
- data/lists/developer_domains.hosts +108 -0
- data/lists/forum_domains.hosts +65 -0
- data/lists/gaming_domains.hosts +99 -0
- data/lists/messaging_domains.hosts +96 -0
- data/lists/music_domains.hosts +62 -0
- data/lists/security_domains.hosts +48 -0
- data/lists/social_media_domains.hosts +443 -0
- data/lists/streaming_domains.hosts +41 -0
- data/lists/video_hosting_domains.hosts +203 -175
- data/lists/video_url_patterns.txt +15 -15
- data/url_categorise.gemspec +11 -9
- metadata +69 -20
data/bin/generate_video_lists
CHANGED
|
@@ -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
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
puts
|
|
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
|
|
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
|
|
34
|
-
puts
|
|
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
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
313
|
+
%r{https?://(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})}i,
|
|
170
314
|
# Escaped URLs with backslashes
|
|
171
|
-
|
|
315
|
+
%r{https?:\\/\\/(?:www\\.)?([a-zA-Z0-9-]+\\.[a-zA-Z]{2,})}i,
|
|
172
316
|
# Domain patterns with optional www (escaped)
|
|
173
|
-
/\(
|
|
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(/[
|
|
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(/\(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
292
|
-
file.puts
|
|
293
|
-
file.puts
|
|
294
|
-
file.puts
|
|
295
|
-
file.puts
|
|
296
|
-
file.puts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
355
|
-
file.puts
|
|
356
|
-
file.puts
|
|
357
|
-
file.puts
|
|
358
|
-
file.puts
|
|
359
|
-
file.puts
|
|
360
|
-
file.puts
|
|
361
|
-
file.puts
|
|
362
|
-
file.puts
|
|
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
|
-
|
|
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
|