-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCLAP_Availability_Checker.lua
More file actions
422 lines (357 loc) · 14.4 KB
/
CLAP_Availability_Checker.lua
File metadata and controls
422 lines (357 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
-- =============================================================================
-- CLAP Availability Checker for REAPER
-- Version 1.0
--
-- Scans your installed VST/VST3/AU plugins, checks clapdb.tech for CLAP
-- versions, then cross-references against CLAPs you already have in REAPER.
--
-- Requirements:
-- • REAPER 6.23+ (for EnumInstalledFX API)
-- • curl on PATH (built-in on macOS/Linux; Windows 10 1803+ has it too)
--
-- Usage: Run via Actions > Load ReaScript, or drop into your Scripts folder.
-- =============================================================================
local SCRIPT_VERSION = "1.0"
local CLAPDB_BASE = "https://clapdb.tech"
local CATEGORIES = {
{ slug = "audio-effects", label = "Audio Effects" },
{ slug = "instruments", label = "Instruments" },
{ slug = "midi-effects-generators",label = "MIDI Effects/Generators"},
{ slug = "modular", label = "Modular" },
{ slug = "analysis-metering", label = "Analysis / Metering" },
{ slug = "control-routing", label = "Control / Routing" },
{ slug = "meta-plugins", label = "Meta Plugins" },
{ slug = "utilities", label = "Utilities" },
{ slug = "development", label = "Development" },
}
-- Minimum character length for a fuzzy token match (avoids "EQ", "FX", etc.)
local MIN_TOKEN_LEN = 5
-- =============================================================================
-- UTILITIES
-- =============================================================================
local function msg(s)
reaper.ShowConsoleMsg(s)
end
local function trim(s)
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
--- Normalize a plugin name for comparison:
--- lowercase, collapse spaces, strip everything non-alphanumeric.
local function normalize(s)
if not s then return "" end
s = s:lower()
s = s:gsub("[^%w]", "") -- keep only alphanumeric
return s
end
--- Strip vendor suffix appended by REAPER, e.g. "Pro-Q 3 (FabFilter)" → "Pro-Q 3"
--- Also strip leading "VST:", "VST3:", "AU:", "CLAP:" type prefixes.
local function clean_fx_name(name)
-- Remove type prefix if present
name = name:gsub("^%w+:%s*", "")
-- Remove trailing parenthesised vendor/info
name = name:gsub("%s*%b()%s*$", "")
return trim(name)
end
--- Shell out to curl. Returns body string or nil on failure.
local function curl_fetch(url)
local is_windows = reaper.GetOS():find("Win") ~= nil
local devnull = is_windows and "2>nul" or "2>/dev/null"
local cmd = string.format(
'curl -sL --max-time 15 -A "REAPER-CLAP-Checker/%s" "%s" %s',
SCRIPT_VERSION, url, devnull
)
local f = io.popen(cmd)
if not f then return nil end
local body = f:read("*a")
f:close()
return (body and #body > 0) and body or nil
end
--- Check whether curl is reachable at all.
local function curl_available()
local is_windows = reaper.GetOS():find("Win") ~= nil
local devnull = is_windows and "2>nul" or "2>/dev/null"
local f = io.popen("curl --version " .. devnull)
if not f then return false end
local out = f:read("*l")
f:close()
return out ~= nil and out:find("curl") ~= nil
end
-- =============================================================================
-- CLAPDB.TECH SCRAPER
-- =============================================================================
--- Parse one category page's HTML and return a list of plugin name strings.
--- clapdb.tech renders plugin entries as:
--- <li><a href="/software/NNN/"><strong>Name</strong></a> — ver</li>
--- (Sometimes <strong> wraps the <a>, sometimes vice-versa.)
local function parse_category_html(html)
local names = {}
local seen = {}
-- Primary pattern: anchor around strong, or strong around anchor,
-- both inside a list item that contains "/software/"
for li_body in html:gmatch("<li>(.-)</li>") do
if li_body:find("/software/") then
-- Try both tag orderings
local name = li_body:match("<strong>(.-)</strong>")
or li_body:match("<b>(.-)</b>")
if name then
-- Strip any nested tags (e.g. <a> inside <strong>)
name = name:gsub("<[^>]+>", "")
name = trim(name)
if name ~= "" and not seen[name] then
seen[name] = true
names[#names + 1] = name
end
end
end
end
-- Fallback for minified HTML: look for the pattern directly
if #names == 0 then
for name in html:gmatch('/software/%d+/"[^>]*><strong>(.-)</strong>') do
name = trim(name)
if name ~= "" and not seen[name] then
seen[name] = true
names[#names + 1] = name
end
end
end
return names
end
--- Fetch all categories from clapdb.tech.
--- Returns two values:
--- clap_list : array of original name strings
--- clap_index : map of normalize(name) → original name
local function fetch_clap_db()
local clap_list = {}
local clap_index = {}
local total = 0
msg("Fetching CLAP database from clapdb.tech …\n")
for _, cat in ipairs(CATEGORIES) do
local url = CLAPDB_BASE .. "/category/" .. cat.slug
msg(string.format(" %-30s", cat.label))
local html = curl_fetch(url)
if html then
local names = parse_category_html(html)
for _, name in ipairs(names) do
local key = normalize(name)
if not clap_index[key] then
clap_index[key] = name
clap_list[#clap_list + 1] = name
end
end
total = total + #names
msg(string.format(" %d plugins\n", #names))
else
msg(" FAILED (network error)\n")
end
end
msg(string.format(" ── Total unique CLAP plugins in DB: %d\n\n", #clap_list))
return clap_list, clap_index
end
-- =============================================================================
-- REAPER PLUGIN ENUMERATION
-- =============================================================================
--- Returns an array of tables: { name, ident, fx_type, clean }
--- fx_type is one of: "CLAP", "VST3", "VST", "AU", "JSFX", "Other"
local function enum_installed_fx()
if not reaper.EnumInstalledFX then
return nil, "EnumInstalledFX API not found. Requires REAPER 6.23 or later."
end
local plugins = {}
local idx = 0
while true do
local ok, name, ident = reaper.EnumInstalledFX(idx)
if not ok then break end
local fx_type
if ident:find("^CLAP:") then fx_type = "CLAP"
elseif ident:find("^VST3:") then fx_type = "VST3"
elseif ident:find("^VST:") then fx_type = "VST"
elseif ident:find("^AU:") then fx_type = "AU"
elseif ident:find("^JS:") then fx_type = "JSFX"
else fx_type = "Other"
end
plugins[#plugins + 1] = {
name = name,
ident = ident,
fx_type = fx_type,
clean = clean_fx_name(name),
}
idx = idx + 1
end
return plugins, nil
end
-- =============================================================================
-- MATCHING ENGINE
-- =============================================================================
--- Try to find a CLAP DB entry for a given installed-plugin clean name.
--- Returns the original clapdb name string, or nil.
local function find_clap_match(clean_name, clap_index)
local norm = normalize(clean_name)
if norm == "" then return nil end
-- 1. Exact normalized match
if clap_index[norm] then return clap_index[norm] end
-- 2. DB name is fully contained in installed name (or vice-versa)
-- Guards against short noise tokens with MIN_TOKEN_LEN.
for db_norm, db_orig in pairs(clap_index) do
if #db_norm >= MIN_TOKEN_LEN and #norm >= MIN_TOKEN_LEN then
if norm:find(db_norm, 1, true) or db_norm:find(norm, 1, true) then
return db_orig
end
end
end
return nil
end
-- =============================================================================
-- REPORT FORMATTING
-- =============================================================================
local SEP_THIN = string.rep("─", 60)
local SEP_THICK = string.rep("═", 60)
local function section(title)
msg("\n" .. SEP_THIN .. "\n")
msg(" " .. title .. "\n")
msg(SEP_THIN .. "\n")
end
local function header()
msg(SEP_THICK .. "\n")
msg(" CLAP Availability Checker v" .. SCRIPT_VERSION .. "\n")
msg(" Powered by clapdb.tech\n")
msg(SEP_THICK .. "\n\n")
end
-- =============================================================================
-- MAIN
-- =============================================================================
local function main()
reaper.ClearConsole()
header()
-- ── Preflight: curl check ──────────────────────────────────────────────────
if not curl_available() then
msg("ERROR: curl not found on PATH.\n\n")
msg(" • macOS / Linux : curl is usually pre-installed.\n")
msg(" • Windows 10/11 : curl ships with the OS (System32\\curl.exe).\n")
msg(" If missing, install via: winget install cURL.cURL\n")
return
end
-- ── Enumerate installed plugins ───────────────────────────────────────────
msg("Scanning installed REAPER plugins …\n")
local plugins, err = enum_installed_fx()
if not plugins then
msg("\nERROR: " .. err .. "\n")
return
end
-- Partition into CLAP, non-CLAP (skip JSFX — not relevant here)
local installed_clap = {}
local installed_non_clap = {}
local clap_norm_index = {} -- normalize(clean) → clean, for dedup check
for _, p in ipairs(plugins) do
if p.fx_type == "CLAP" then
installed_clap[#installed_clap + 1] = p
clap_norm_index[normalize(p.clean)] = p.clean
elseif p.fx_type ~= "JSFX" and p.fx_type ~= "Other" then
installed_non_clap[#installed_non_clap + 1] = p
end
end
msg(string.format(" Total plugins : %d\n", #plugins))
msg(string.format(" CLAP (installed): %d\n", #installed_clap))
msg(string.format(" VST/VST3/AU : %d\n\n", #installed_non_clap))
-- ── Fetch CLAP DB ─────────────────────────────────────────────────────────
local _, clap_db = fetch_clap_db()
if not next(clap_db) then
msg("ERROR: CLAP database returned no results.\n")
msg("Check your internet connection and try again.\n")
return
end
-- ── Compare ───────────────────────────────────────────────────────────────
-- Track seen clean names to avoid double-reporting VST + VST3 of same plugin
local reported_clean = {}
local upgradeable = {} -- { clean, fx_type, clap_db_name, already_clap }
local skipped = {} -- already have it as CLAP
for _, p in ipairs(installed_non_clap) do
local cn = p.clean
if cn == "" then goto continue end
-- Skip if we've already handled this name (e.g. both VST and VST3 installed)
if reported_clean[normalize(cn)] then goto continue end
reported_clean[normalize(cn)] = true
-- Does the user already have a CLAP version of this plugin?
if clap_norm_index[normalize(cn)] then
skipped[#skipped + 1] = { clean = cn, fx_type = p.fx_type }
goto continue
end
-- Is there a CLAP version available on clapdb.tech?
local match = find_clap_match(cn, clap_db)
if match then
upgradeable[#upgradeable + 1] = {
clean = cn,
fx_type = p.fx_type,
clap_db_name = match,
}
end
::continue::
end
-- ── Print results ─────────────────────────────────────────────────────────
section(string.format("CLAP versions available (%d)", #upgradeable))
if #upgradeable == 0 then
msg("\n Nothing found — either all CLAP versions are already installed,\n")
msg(" or your installed plugins are not yet listed on clapdb.tech.\n")
else
-- Group by format type for readability
local by_type = {}
for _, r in ipairs(upgradeable) do
if not by_type[r.fx_type] then by_type[r.fx_type] = {} end
by_type[r.fx_type][#by_type[r.fx_type] + 1] = r
end
local type_order = { "VST3", "VST", "AU" }
for _, ft in ipairs(type_order) do
local list = by_type[ft]
if list then
-- Sort alphabetically by clean name
table.sort(list, function(a, b)
return a.clean:lower() < b.clean:lower()
end)
msg(string.format("\n [%s]\n", ft))
for _, r in ipairs(list) do
-- Show DB name only when it differs from installed name
local note = ""
if normalize(r.clean) ~= normalize(r.clap_db_name) then
note = string.format(" (listed as "%s")", r.clap_db_name)
end
msg(string.format(" ► %-40s%s\n", r.clean, note))
end
end
end
end
-- ── Already-installed CLAP summary ────────────────────────────────────────
if #skipped > 0 then
section(string.format("Already using CLAP — skipped (%d)", #skipped))
table.sort(skipped, function(a, b)
return a.clean:lower() < b.clean:lower()
end)
for _, r in ipairs(skipped) do
msg(string.format(" ✓ %s (also installed as %s)\n", r.clean, r.fx_type))
end
end
-- ── Footer ────────────────────────────────────────────────────────────────
msg("\n" .. SEP_THICK .. "\n")
msg(" Find download links at https://clapdb.tech\n")
msg(SEP_THICK .. "\n")
-- Offer to open browser
local answer = reaper.ShowMessageBox(
string.format(
"%d plugin(s) have CLAP versions available.\n\n"..
"Open clapdb.tech in your browser?",
#upgradeable
),
"CLAP Availability Checker",
4 -- Yes / No
)
if answer == 6 then -- 6 = Yes
local url = CLAPDB_BASE
local platform = reaper.GetOS()
if platform:find("Win") then
os.execute('start "" "' .. url .. '"')
elseif platform:find("OSX") or platform:find("macOS") then
os.execute('open "' .. url .. '"')
else
os.execute('xdg-open "' .. url .. '"')
end
end
end
main()