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
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'time'
|
|
9
|
+
require 'set'
|
|
10
|
+
|
|
11
|
+
module VideoListGenerator
|
|
12
|
+
class FocusedExtractorParser
|
|
13
|
+
# Focus on the most popular video hosting platforms first
|
|
14
|
+
PRIORITY_EXTRACTORS = [
|
|
15
|
+
'youtube.py',
|
|
16
|
+
'vimeo.py',
|
|
17
|
+
'dailymotion.py',
|
|
18
|
+
'twitch.py',
|
|
19
|
+
'tiktok.py',
|
|
20
|
+
'instagram.py',
|
|
21
|
+
'facebook.py',
|
|
22
|
+
'twitter.py',
|
|
23
|
+
'reddit.py',
|
|
24
|
+
'streamable.py',
|
|
25
|
+
'wistia.py',
|
|
26
|
+
'brightcove.py',
|
|
27
|
+
'jwplatform.py',
|
|
28
|
+
'kaltura.py',
|
|
29
|
+
'vidyard.py',
|
|
30
|
+
'vzaar.py',
|
|
31
|
+
'soundcloud.py',
|
|
32
|
+
'mixcloud.py',
|
|
33
|
+
'bandcamp.py',
|
|
34
|
+
'peertube.py',
|
|
35
|
+
'rumble.py',
|
|
36
|
+
'odysee.py',
|
|
37
|
+
'bitchute.py'
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
YT_DLP_API_URL = 'https://api.github.com/repos/yt-dlp/yt-dlp/contents/yt_dlp/extractor'
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@video_domains = Set.new
|
|
44
|
+
@video_regexes = []
|
|
45
|
+
@manual_domains = get_manual_video_domains
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def generate_lists
|
|
49
|
+
puts "🎬 Generating focused video hosting lists..."
|
|
50
|
+
|
|
51
|
+
puts "📝 Adding manually curated video hosting domains..."
|
|
52
|
+
@manual_domains.each { |domain| @video_domains.add(domain) }
|
|
53
|
+
|
|
54
|
+
puts "📡 Fetching priority yt-dlp extractors..."
|
|
55
|
+
fetch_priority_extractors
|
|
56
|
+
|
|
57
|
+
create_lists_directory
|
|
58
|
+
generate_hosts_file
|
|
59
|
+
generate_regex_file
|
|
60
|
+
|
|
61
|
+
puts "✅ Video hosting lists generated successfully!"
|
|
62
|
+
puts "📁 Files created in lists/ directory:"
|
|
63
|
+
puts " - video_hosting_domains.hosts (PiHole compatible)"
|
|
64
|
+
puts " - video_url_patterns.txt (Regex patterns for video detection)"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def get_manual_video_domains
|
|
70
|
+
# Manually curated list of major video hosting domains
|
|
71
|
+
[
|
|
72
|
+
'youtube.com', 'youtu.be', 'youtube-nocookie.com',
|
|
73
|
+
'vimeo.com', 'player.vimeo.com',
|
|
74
|
+
'dailymotion.com', 'dai.ly',
|
|
75
|
+
'twitch.tv', 'clips.twitch.tv',
|
|
76
|
+
'tiktok.com', 'vm.tiktok.com',
|
|
77
|
+
'instagram.com', 'instagr.am',
|
|
78
|
+
'facebook.com', 'fb.com', 'fb.watch',
|
|
79
|
+
'twitter.com', 'x.com', 't.co',
|
|
80
|
+
'reddit.com', 'redd.it', 'v.redd.it',
|
|
81
|
+
'streamable.com',
|
|
82
|
+
'wistia.com', 'fast.wistia.com', 'fast.wistia.net',
|
|
83
|
+
'brightcove.com', 'bcove.video',
|
|
84
|
+
'jwplatform.com', 'jwpcdn.com',
|
|
85
|
+
'kaltura.com',
|
|
86
|
+
'vidyard.com',
|
|
87
|
+
'vzaar.com',
|
|
88
|
+
'soundcloud.com', 'snd.sc',
|
|
89
|
+
'mixcloud.com',
|
|
90
|
+
'bandcamp.com',
|
|
91
|
+
'rumble.com',
|
|
92
|
+
'odysee.com', 'lbry.tv',
|
|
93
|
+
'bitchute.com',
|
|
94
|
+
'peertube.tv',
|
|
95
|
+
'archive.org',
|
|
96
|
+
'vevo.com',
|
|
97
|
+
'liveleak.com',
|
|
98
|
+
'metacafe.com',
|
|
99
|
+
'veoh.com',
|
|
100
|
+
'break.com',
|
|
101
|
+
'crunchyroll.com',
|
|
102
|
+
'funimation.com',
|
|
103
|
+
'vrv.co',
|
|
104
|
+
'roosterteeth.com',
|
|
105
|
+
'newgrounds.com',
|
|
106
|
+
'coub.com',
|
|
107
|
+
'vidme.com',
|
|
108
|
+
'vid.me',
|
|
109
|
+
'gfycat.com',
|
|
110
|
+
'imgur.com',
|
|
111
|
+
'giphy.com',
|
|
112
|
+
'tenor.com'
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def fetch_priority_extractors
|
|
117
|
+
uri = URI(YT_DLP_API_URL)
|
|
118
|
+
response = Net::HTTP.get_response(uri)
|
|
119
|
+
|
|
120
|
+
unless response.code == '200'
|
|
121
|
+
puts "⚠️ Failed to fetch extractor list from GitHub API, using manual domains only"
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
files = JSON.parse(response.body)
|
|
126
|
+
|
|
127
|
+
# Filter to priority extractor files
|
|
128
|
+
priority_files = files.select { |file| PRIORITY_EXTRACTORS.include?(file['name']) }
|
|
129
|
+
|
|
130
|
+
puts "📝 Processing #{priority_files.length} priority extractor files..."
|
|
131
|
+
|
|
132
|
+
priority_files.each_with_index do |file, index|
|
|
133
|
+
puts "Processing #{index + 1}/#{priority_files.length}: #{file['name']}"
|
|
134
|
+
process_extractor_file(file)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
puts "🎯 Found #{@video_domains.size} unique video hosting domains"
|
|
138
|
+
puts "🔍 Extracted #{@video_regexes.size} URL pattern regexes"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def process_extractor_file(file_info)
|
|
142
|
+
begin
|
|
143
|
+
response = Net::HTTP.get_response(URI(file_info['download_url']))
|
|
144
|
+
return unless response.code == '200'
|
|
145
|
+
|
|
146
|
+
content = response.body
|
|
147
|
+
|
|
148
|
+
extract_domains_from_content(content, file_info['name'])
|
|
149
|
+
extract_regexes_from_content(content, file_info['name'])
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
puts "⚠️ Error processing #{file_info['name']}: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def extract_domains_from_content(content, filename)
|
|
156
|
+
# Look for _VALID_URL patterns
|
|
157
|
+
valid_url_patterns = content.scan(/_VALID_URL\s*=\s*r?['"]([^'"]+)['"]/m)
|
|
158
|
+
|
|
159
|
+
valid_url_patterns.each do |pattern|
|
|
160
|
+
pattern = pattern[0] if pattern.is_a?(Array)
|
|
161
|
+
|
|
162
|
+
domains = extract_domains_from_regex(pattern)
|
|
163
|
+
domains.each { |domain| @video_domains.add(domain) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Also look for explicit domain lists or IE_NAME patterns
|
|
167
|
+
ie_name_patterns = content.scan(/IE_NAME\s*=\s*['"]([^'"]+)['"]/m)
|
|
168
|
+
ie_name_patterns.each do |name|
|
|
169
|
+
name = name[0] if name.is_a?(Array)
|
|
170
|
+
next unless name
|
|
171
|
+
|
|
172
|
+
# Many IE_NAME values correspond to domain names
|
|
173
|
+
if name.include?('.') && valid_domain?(name)
|
|
174
|
+
@video_domains.add(name.downcase)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def extract_regexes_from_content(content, filename)
|
|
180
|
+
# Extract _VALID_URL patterns for video detection
|
|
181
|
+
valid_url_patterns = content.scan(/_VALID_URL\s*=\s*r?['"]([^'"]+)['"]/m)
|
|
182
|
+
|
|
183
|
+
valid_url_patterns.each do |pattern|
|
|
184
|
+
pattern = pattern[0] if pattern.is_a?(Array)
|
|
185
|
+
|
|
186
|
+
cleaned_pattern = clean_regex_pattern(pattern)
|
|
187
|
+
if cleaned_pattern && !cleaned_pattern.empty?
|
|
188
|
+
@video_regexes << {
|
|
189
|
+
source: filename.gsub('.py', ''),
|
|
190
|
+
pattern: cleaned_pattern,
|
|
191
|
+
original: pattern,
|
|
192
|
+
description: generate_pattern_description(pattern, filename)
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def extract_domains_from_regex(pattern)
|
|
199
|
+
domains = Set.new
|
|
200
|
+
|
|
201
|
+
# Various patterns to extract domains from regex
|
|
202
|
+
# Pattern: https?://(?:www\.)?domain\.com
|
|
203
|
+
domain_matches = pattern.scan(/https?:\/\/[^\/]*?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i)
|
|
204
|
+
domain_matches.each do |match|
|
|
205
|
+
domain = match[0] if match.is_a?(Array)
|
|
206
|
+
domain = clean_domain(domain) if domain
|
|
207
|
+
domains.add(domain.downcase) if domain && valid_domain?(domain)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Pattern: (?:www\.)?domain\.com
|
|
211
|
+
simple_matches = pattern.scan(/(?:\(\?\:www\\\.\)\?)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/i)
|
|
212
|
+
simple_matches.each do |match|
|
|
213
|
+
domain = match[0] if match.is_a?(Array)
|
|
214
|
+
domain = clean_domain(domain) if domain
|
|
215
|
+
domains.add(domain.downcase) if domain && valid_domain?(domain)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
domains.to_a
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def clean_domain(domain)
|
|
222
|
+
return nil unless domain
|
|
223
|
+
|
|
224
|
+
# Remove common regex escapes
|
|
225
|
+
cleaned = domain.gsub(/\\\./, '.').gsub(/[\(\)\?\:\|\\]/, '')
|
|
226
|
+
cleaned = cleaned.strip
|
|
227
|
+
|
|
228
|
+
# Remove any trailing regex syntax
|
|
229
|
+
cleaned = cleaned.split(/[\(\)\[\]]/)[0] if cleaned.include?('(') || cleaned.include?('[')
|
|
230
|
+
|
|
231
|
+
cleaned
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def clean_regex_pattern(pattern)
|
|
235
|
+
return nil unless pattern
|
|
236
|
+
|
|
237
|
+
# Simplify common patterns for practical use
|
|
238
|
+
cleaned = pattern.dup
|
|
239
|
+
|
|
240
|
+
# Remove complex lookaheads/lookbehinds
|
|
241
|
+
cleaned = cleaned.gsub(/\(\?\![^)]*\)/, '')
|
|
242
|
+
cleaned = cleaned.gsub(/\(\?\<\![^)]*\)/, '')
|
|
243
|
+
|
|
244
|
+
# Simplify common patterns
|
|
245
|
+
cleaned = cleaned.gsub(/\(\?\:www\\\.\)\?/, '(?:www\.)?')
|
|
246
|
+
cleaned = cleaned.gsub(/\\\./, '\.')
|
|
247
|
+
|
|
248
|
+
# Validate that it's a reasonable regex
|
|
249
|
+
begin
|
|
250
|
+
Regexp.new(cleaned)
|
|
251
|
+
cleaned
|
|
252
|
+
rescue RegexpError
|
|
253
|
+
# Return a simplified version if the complex regex fails
|
|
254
|
+
extract_simple_pattern(pattern)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def extract_simple_pattern(pattern)
|
|
259
|
+
# Extract a simple domain pattern from complex regex
|
|
260
|
+
if pattern.include?('https?://')
|
|
261
|
+
# Extract the basic domain part
|
|
262
|
+
match = pattern.match(/https?:\/\/[^\/]*?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i)
|
|
263
|
+
if match
|
|
264
|
+
return "https?://[^/]*#{Regexp.escape(match[1])}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def generate_pattern_description(pattern, filename)
|
|
272
|
+
service = filename.gsub('.py', '').capitalize
|
|
273
|
+
|
|
274
|
+
case filename
|
|
275
|
+
when 'youtube.py'
|
|
276
|
+
'YouTube video, playlist, and channel URLs'
|
|
277
|
+
when 'vimeo.py'
|
|
278
|
+
'Vimeo video and album URLs'
|
|
279
|
+
when 'twitch.py'
|
|
280
|
+
'Twitch streams, clips, and VODs'
|
|
281
|
+
when 'tiktok.py'
|
|
282
|
+
'TikTok video URLs'
|
|
283
|
+
else
|
|
284
|
+
"#{service} video content URLs"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def valid_domain?(domain)
|
|
289
|
+
return false unless domain
|
|
290
|
+
return false if domain.length < 4
|
|
291
|
+
return false unless domain.match?(/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
|
|
292
|
+
return false if domain.include?('example.') || domain.include?('test.')
|
|
293
|
+
return false if domain.include?('localhost')
|
|
294
|
+
true
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def create_lists_directory
|
|
298
|
+
FileUtils.mkdir_p('lists')
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def generate_hosts_file
|
|
302
|
+
hosts_file_path = 'lists/video_hosting_domains.hosts'
|
|
303
|
+
|
|
304
|
+
File.open(hosts_file_path, 'w') do |file|
|
|
305
|
+
write_hosts_header(file)
|
|
306
|
+
|
|
307
|
+
@video_domains.sort.each do |domain|
|
|
308
|
+
file.puts "0.0.0.0 #{domain}"
|
|
309
|
+
file.puts "0.0.0.0 www.#{domain}" unless domain.start_with?('www.')
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
puts "📝 Generated hosts file: #{hosts_file_path} (#{@video_domains.size} domains)"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def generate_regex_file
|
|
317
|
+
regex_file_path = 'lists/video_url_patterns.txt'
|
|
318
|
+
|
|
319
|
+
File.open(regex_file_path, 'w') do |file|
|
|
320
|
+
write_regex_header(file)
|
|
321
|
+
|
|
322
|
+
@video_regexes.each do |regex_info|
|
|
323
|
+
file.puts "# Source: #{regex_info[:source]}"
|
|
324
|
+
file.puts "# Description: #{regex_info[:description]}"
|
|
325
|
+
file.puts "# Pattern: #{regex_info[:pattern]}"
|
|
326
|
+
file.puts "# Original: #{regex_info[:original]}"
|
|
327
|
+
file.puts regex_info[:pattern]
|
|
328
|
+
file.puts ""
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
puts "📝 Generated regex file: #{regex_file_path} (#{@video_regexes.size} patterns)"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def write_hosts_header(file)
|
|
336
|
+
file.puts "# Video Hosting Domains - PiHole Compatible Hosts File"
|
|
337
|
+
file.puts "# Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
338
|
+
file.puts "# Source: yt-dlp extractors + manual curation"
|
|
339
|
+
file.puts "# Purpose: Block or categorize video hosting websites"
|
|
340
|
+
file.puts "# Format: 0.0.0.0 domain.com"
|
|
341
|
+
file.puts "#"
|
|
342
|
+
file.puts "# This file contains domains from popular video hosting platforms"
|
|
343
|
+
file.puts "# extracted from yt-dlp video extractors and manually curated lists."
|
|
344
|
+
file.puts "# Use this for URL categorization or content filtering."
|
|
345
|
+
file.puts "#"
|
|
346
|
+
file.puts "# Generated by UrlCategorise gem: https://github.com/trex22/url_categorise"
|
|
347
|
+
file.puts "#"
|
|
348
|
+
file.puts ""
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def write_regex_header(file)
|
|
352
|
+
file.puts "# Video URL Detection Patterns"
|
|
353
|
+
file.puts "# Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
354
|
+
file.puts "# Source: yt-dlp extractors + manual curation"
|
|
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 (matches these patterns)"
|
|
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 "# Each pattern includes:"
|
|
365
|
+
file.puts "# - Source extractor name"
|
|
366
|
+
file.puts "# - Human-readable description"
|
|
367
|
+
file.puts "# - Simplified regex pattern"
|
|
368
|
+
file.puts "# - Original complex pattern (for reference)"
|
|
369
|
+
file.puts "#"
|
|
370
|
+
file.puts "# Generated by UrlCategorise gem: https://github.com/trex22/url_categorise"
|
|
371
|
+
file.puts "#"
|
|
372
|
+
file.puts ""
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Run the generator if this file is executed directly
|
|
378
|
+
if __FILE__ == $0
|
|
379
|
+
generator = VideoListGenerator::FocusedExtractorParser.new
|
|
380
|
+
generator.generate_lists
|
|
381
|
+
end
|
data/correct_usage_example.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require 'bundler/setup'
|
|
4
4
|
require './lib/url_categorise'
|
|
5
5
|
|
|
6
|
-
puts
|
|
6
|
+
puts '=== Large Dataset Loading Example ==='
|
|
7
7
|
|
|
8
8
|
# Configuration for handling large datasets (300+ MB)
|
|
9
9
|
# First test with cache-only mode
|
|
10
|
-
puts
|
|
10
|
+
puts 'Creating client with cached datasets only...'
|
|
11
11
|
client = UrlCategorise::Client.new(
|
|
12
12
|
cache_dir: './url_cache',
|
|
13
13
|
auto_load_datasets: true,
|
|
@@ -19,46 +19,46 @@ client = UrlCategorise::Client.new(
|
|
|
19
19
|
}
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
puts
|
|
23
|
-
puts
|
|
24
|
-
puts
|
|
22
|
+
puts 'Client created successfully!'
|
|
23
|
+
puts ''
|
|
24
|
+
puts 'Dataset Statistics:'
|
|
25
25
|
puts " Total categories: #{client.count_of_categories}"
|
|
26
26
|
puts " Dataset categories: #{client.count_of_dataset_categories}"
|
|
27
27
|
puts " Blocklist categories: #{client.count_of_categories - client.count_of_dataset_categories}"
|
|
28
|
-
puts
|
|
28
|
+
puts ''
|
|
29
29
|
puts " Total hosts: #{client.count_of_hosts.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
|
|
30
30
|
puts " Dataset hosts: #{client.count_of_dataset_hosts.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
|
|
31
|
-
puts
|
|
31
|
+
puts ''
|
|
32
32
|
puts " Total data size: #{client.size_of_data.round(1)} MB"
|
|
33
|
-
puts " Dataset data size: #{client.size_of_dataset_data.round(1)} MB"
|
|
33
|
+
puts " Dataset data size: #{client.size_of_dataset_data.round(1)} MB"
|
|
34
34
|
puts " Blocklist data size: #{client.size_of_blocklist_data.round(1)} MB"
|
|
35
35
|
|
|
36
|
-
puts
|
|
37
|
-
puts
|
|
36
|
+
puts ''
|
|
37
|
+
puts 'Dataset-specific Statistics:'
|
|
38
38
|
# Get dataset metadata if available
|
|
39
39
|
metadata = client.dataset_metadata
|
|
40
40
|
if metadata && !metadata.empty?
|
|
41
41
|
puts " Datasets loaded: #{metadata.size}"
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# Calculate size for each dataset by finding its categories and domains
|
|
44
|
-
|
|
44
|
+
client.instance_variable_get(:@dataset_categories)
|
|
45
45
|
total_dataset_size = 0
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
metadata.each_with_index do |(hash, data), index|
|
|
48
48
|
# Estimate size contribution of this dataset
|
|
49
49
|
dataset_portion = data[:total_entries].to_f / metadata.values.sum { |d| d[:total_entries] }
|
|
50
50
|
dataset_size_mb = (client.size_of_dataset_data * dataset_portion).round(2)
|
|
51
51
|
total_dataset_size += dataset_size_mb
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
puts " Dataset #{index + 1}:"
|
|
54
54
|
puts " Processed at: #{data[:processed_at]}"
|
|
55
55
|
puts " Total entries: #{data[:total_entries].to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
|
|
56
56
|
puts " Estimated size: #{dataset_size_mb} MB"
|
|
57
57
|
puts " Data hash: #{hash[0..12]}..."
|
|
58
58
|
end
|
|
59
|
-
|
|
60
|
-
puts
|
|
59
|
+
|
|
60
|
+
puts ''
|
|
61
61
|
puts " Total dataset size: #{total_dataset_size.round(2)} MB (#{client.size_of_dataset_data.round(1)} MB actual)"
|
|
62
62
|
else
|
|
63
|
-
puts
|
|
64
|
-
end
|
|
63
|
+
puts ' No dataset metadata available'
|
|
64
|
+
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative "models"
|
|
2
2
|
|
|
3
3
|
module UrlCategorise
|
|
4
4
|
class ActiveRecordClient < Client
|
|
5
5
|
def initialize(**kwargs)
|
|
6
|
-
raise
|
|
6
|
+
raise "ActiveRecord not available" unless UrlCategorise::Models.available?
|
|
7
7
|
|
|
8
8
|
@use_database = kwargs.delete(:use_database) { true }
|
|
9
9
|
super(**kwargs)
|
|
@@ -14,7 +14,7 @@ module UrlCategorise
|
|
|
14
14
|
def categorise(url)
|
|
15
15
|
return super(url) unless @use_database && UrlCategorise::Models.available?
|
|
16
16
|
|
|
17
|
-
host = (URI.parse(url).host || url).downcase.gsub(
|
|
17
|
+
host = (URI.parse(url).host || url).downcase.gsub("www.", "")
|
|
18
18
|
|
|
19
19
|
# Try database first
|
|
20
20
|
categories = UrlCategorise::Models::Domain.categorise(host)
|
|
@@ -59,7 +59,7 @@ module UrlCategorise
|
|
|
59
59
|
# Store dataset metadata in database if enabled
|
|
60
60
|
if @use_database && UrlCategorise::Models.available? && @dataset_metadata
|
|
61
61
|
store_dataset_metadata_in_db(
|
|
62
|
-
source_type:
|
|
62
|
+
source_type: "kaggle",
|
|
63
63
|
identifier: "#{dataset_owner}/#{dataset_name}",
|
|
64
64
|
metadata: @dataset_metadata.values.last,
|
|
65
65
|
category_mappings: options[:category_mappings],
|
|
@@ -79,7 +79,7 @@ module UrlCategorise
|
|
|
79
79
|
# Store dataset metadata in database if enabled
|
|
80
80
|
if @use_database && UrlCategorise::Models.available? && @dataset_metadata
|
|
81
81
|
store_dataset_metadata_in_db(
|
|
82
|
-
source_type:
|
|
82
|
+
source_type: "csv",
|
|
83
83
|
identifier: url,
|
|
84
84
|
metadata: @dataset_metadata.values.last,
|
|
85
85
|
category_mappings: options[:category_mappings],
|
|
@@ -125,7 +125,7 @@ module UrlCategorise
|
|
|
125
125
|
metadata = @metadata[url] || {}
|
|
126
126
|
UrlCategorise::Models::ListMetadata.find_or_create_by(url: url) do |record|
|
|
127
127
|
record.name = category.to_s
|
|
128
|
-
record.categories = [category.to_s]
|
|
128
|
+
record.categories = [ category.to_s ]
|
|
129
129
|
record.file_hash = metadata[:content_hash]
|
|
130
130
|
record.fetched_at = metadata[:last_updated]
|
|
131
131
|
end
|
|
@@ -140,12 +140,12 @@ module UrlCategorise
|
|
|
140
140
|
existing = UrlCategorise::Models::Domain.find_by(domain: domain)
|
|
141
141
|
if existing
|
|
142
142
|
# Add category if not already present
|
|
143
|
-
categories = existing.categories | [category.to_s]
|
|
143
|
+
categories = existing.categories | [ category.to_s ]
|
|
144
144
|
existing.update(categories: categories) if categories != existing.categories
|
|
145
145
|
else
|
|
146
146
|
UrlCategorise::Models::Domain.create!(
|
|
147
147
|
domain: domain,
|
|
148
|
-
categories: [category.to_s]
|
|
148
|
+
categories: [ category.to_s ]
|
|
149
149
|
)
|
|
150
150
|
end
|
|
151
151
|
end
|
|
@@ -163,12 +163,12 @@ module UrlCategorise
|
|
|
163
163
|
|
|
164
164
|
existing = UrlCategorise::Models::IpAddress.find_by(ip_address: ip)
|
|
165
165
|
if existing
|
|
166
|
-
categories = existing.categories | [category.to_s]
|
|
166
|
+
categories = existing.categories | [ category.to_s ]
|
|
167
167
|
existing.update(categories: categories) if categories != existing.categories
|
|
168
168
|
else
|
|
169
169
|
UrlCategorise::Models::IpAddress.create!(
|
|
170
170
|
ip_address: ip,
|
|
171
|
-
categories: [category.to_s]
|
|
171
|
+
categories: [ category.to_s ]
|
|
172
172
|
)
|
|
173
173
|
end
|
|
174
174
|
end
|
|
@@ -189,7 +189,7 @@ module UrlCategorise
|
|
|
189
189
|
end
|
|
190
190
|
rescue ActiveRecord::RecordInvalid => e
|
|
191
191
|
# Dataset metadata already exists or validation failed
|
|
192
|
-
puts "Warning: Failed to store dataset metadata: #{e.message}" if ENV[
|
|
192
|
+
puts "Warning: Failed to store dataset metadata: #{e.message}" if ENV["DEBUG"]
|
|
193
193
|
end
|
|
194
194
|
end
|
|
195
195
|
end
|