Migration 1200 passed regexes with `?` through knex, which made them bindings: the live function held blu-$1ray, web-$2dl and h\.$326[45], so BluRay, WEB-DL and H.264 names without a resolution were Misc. 93% of recent releases were Misc, as the category came from the name alone. category becomes a plain column (DROP EXPRESSION, no rewrite) set by a trigger: the name's category (fixed, no `?` anywhere; adds XXX, audiobooks, console and PC games, and TV codes like S01e07[ and S04E01E02), else the prober's inner name's, else one given before (a spot's), else the groups'. The spot inspector sets a spot's category (spot_category) on Misc releases it links and on its own releases. The Newznab and web category lists gain Console, PC Games, Audiobook, Anime, XXX and Comics. releases:recategorize recomputes existing rows in id batches, writing only changed rows and waiting under a free-space floor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
8.4 KiB
TypeScript
129 lines
8.4 KiB
TypeScript
import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
|
|
/**
|
|
* A release's category from more than its name (REBUILD.md, part 9; LOG.md 2026-10-23 (95)). Migration 1200 made it
|
|
* a generated column of release_category(name), and knex took the `?` in its regexes for bindings: the function held
|
|
* `blu-$1ray`, `web-$2dl`, `h\.$326[45]`, so BluRay, WEB-DL and H.264 names without a resolution were Misc. Now:
|
|
* - release_category(name), fixed (no `?` in any regex here: `{0,1}`) and with XXX, audiobooks, console and PC games,
|
|
* and TV codes the old one missed (`S01e07[`, `S04E01E02`);
|
|
* - the inner name the prober found (an obfuscated release's real name) when the name says nothing;
|
|
* - else a category given before (a spot's, below), else the groups' (erotica, sounds, ebooks, …);
|
|
* - a spot's category (spot_category: its head category and subcategories), set by the spot inspector.
|
|
* `category` becomes a plain column (DROP EXPRESSION keeps the values, no rewrite) set by a trigger; existing rows are
|
|
* recategorized by `releases:recategorize` in batches, not here (a rewrite of the 70 GB table).
|
|
*/
|
|
export default class extends BaseSchema {
|
|
async up() {
|
|
this.schema.raw(String.raw`
|
|
CREATE OR REPLACE FUNCTION release_category(name text) RETURNS integer IMMUTABLE LANGUAGE sql AS $$
|
|
SELECT CASE
|
|
-- XXX first: its names carry years, resolutions and episode-like codes too.
|
|
WHEN name ~* '(^|[^a-z0-9])xxx([^a-z0-9]|$)' THEN
|
|
CASE WHEN name ~* '2160p|(^|[._ -])(uhd|4k)([._ -]|$)' THEN 6045
|
|
WHEN name ~* '720p|1080p|x26[45]|h\.{0,1}26[45]|hevc' THEN 6040
|
|
WHEN name ~* 'imageset|picset|(^|[._ -])pics([._ -]|$)' THEN 6060 ELSE 6070 END
|
|
-- TV: S01E02 (also S01e02[, S04E01E02), 1x02, or a season pack (S01).
|
|
WHEN name ~* '(^|[^a-z0-9])(s[0-9]{1,2}[ ._-]{0,1}e[0-9]{1,3}|[0-9]{1,2}x[0-9]{2})([^0-9]|$)' OR name ~* '(^|[._ -])s[0-9]{1,2}([._ -]|$)' THEN
|
|
CASE WHEN name ~* '2160p|(^|[._ -])(uhd|4k)([._ -]|$)' THEN 5045
|
|
WHEN name ~* '720p|1080p' THEN 5040 ELSE 5030 END
|
|
-- Movies: a year plus a source or quality marker.
|
|
WHEN name ~* '(^|[._ (-])(19|20)[0-9]{2}([._ )-]|$)'
|
|
AND name ~* '(480p|576p|720p|1080p|2160p|blu-{0,1}ray|web-{0,1}dl|webrip|bdrip|brrip|dvdrip|hdtv|x26[45]|h\.{0,1}26[45]|hevc|remux)' THEN
|
|
CASE WHEN name ~* '2160p|(^|[._ -])(uhd|4k)([._ -]|$)' THEN 2045
|
|
WHEN name ~* '720p|1080p|blu-{0,1}ray' THEN 2040 ELSE 2030 END
|
|
-- Audio: audiobooks, lossless, mp3, or scene music names (-WEB-2026-GROUP).
|
|
WHEN name ~* '(audiobook|audioboek|luisterboek|h(oe|ö)rbuch)' THEN 3030
|
|
WHEN name ~* '(^|[._ -])flac([._ -]|$)' THEN 3040
|
|
WHEN name ~* '(^|[._ -])mp3([._ -]|$)' OR name ~* '-(WEB|CD|CDM|CDS|EP|VINYL|VLS|SAT|FM|DAB)-(19|20)[0-9]{2}-[A-Za-z0-9]+$' THEN 3010
|
|
WHEN name ~* '(^|[._ -])(epub|ebook|e-book|mobi|azw3)([._ -]|$)' THEN 7020
|
|
-- Games: console tags, PC scene groups and repackers.
|
|
WHEN name ~* '(^|[._ -])(nsw|ps4|ps5|xboxone|xbox360|wiiu|3ds|nds)([._ -]|$)' THEN 1000
|
|
WHEN name ~* '-(codex|plaza|skidrow|reloaded|flt|doge|tenoke|rune|empress|razor1911|tinyiso|darksiders|simplex)$' OR name ~* '(fitgirl|dodi|elamigos)[._ -]{0,1}repack' THEN 4050
|
|
ELSE 8010
|
|
END
|
|
$$`)
|
|
this.schema.raw(String.raw`
|
|
CREATE OR REPLACE FUNCTION release_group_category(groups jsonb) RETURNS integer IMMUTABLE LANGUAGE sql AS $$
|
|
SELECT CASE
|
|
WHEN g ~* '(erotica|xxx|porn|erotic|nude|naked)' THEN CASE WHEN g ~* '(pictures|pics|images|photos)' THEN 6060 ELSE 6070 END
|
|
WHEN g ~* 'anime' THEN 5070
|
|
WHEN g ~* 'audiobook' THEN 3030
|
|
WHEN g ~* '(sounds|mp3|music)' THEN CASE WHEN g ~* '(lossless|flac)' THEN 3040 ELSE 3010 END
|
|
WHEN g ~* 'comics' THEN 7030
|
|
WHEN g ~* 'e-{0,1}books{0,1}' THEN 7020
|
|
WHEN g ~* '(games|nintendo|xbox|playstation|wii)' THEN 1000
|
|
WHEN g ~* '(^|[. ])(tv|hdtv|teevee|tvseries)([. ]|$)' THEN 5000
|
|
WHEN g ~* '(^|[. ])(movies|dvd|bluray|dvdr)([. ]|$)' THEN 2000
|
|
ELSE 8010
|
|
END
|
|
FROM (SELECT coalesce(string_agg(value, ' '), '') AS g FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(groups) = 'array' THEN groups ELSE '[]' END)) x
|
|
$$`)
|
|
// The name's, else the inner name's, else one given before (a spot's), else the groups'.
|
|
this.schema.raw(`
|
|
CREATE OR REPLACE FUNCTION release_category_for(name text, inner_name text, groups jsonb, given integer) RETURNS integer IMMUTABLE LANGUAGE sql AS $$
|
|
SELECT CASE WHEN n <> 8010 THEN n WHEN i <> 8010 THEN i WHEN coalesce(given, 8010) <> 8010 THEN given ELSE release_group_category(groups) END
|
|
FROM (SELECT release_category(name) AS n, coalesce(release_category(inner_name), 8010) AS i) x
|
|
$$`)
|
|
// Whether a spot's subcategories of one type letter ({"a": [9], "z": [0]}) hold one of `vals`.
|
|
this.schema.raw(`
|
|
CREATE OR REPLACE FUNCTION spot_has(subcats jsonb, letter text, vals integer[]) RETURNS boolean IMMUTABLE LANGUAGE sql AS $$
|
|
SELECT coalesce((SELECT bool_or((e #>> '{}')::integer = ANY(vals)) FROM jsonb_array_elements(
|
|
CASE WHEN jsonb_typeof(subcats->letter) = 'array' THEN subcats->letter ELSE '[]' END) e), false)
|
|
$$`)
|
|
// A spot's category (lib/spotnet/categories.ts) in Newznab's; null for pictures and the unknown.
|
|
this.schema.raw(`
|
|
CREATE OR REPLACE FUNCTION spot_category(category integer, subcats jsonb) RETURNS integer IMMUTABLE LANGUAGE sql AS $$
|
|
SELECT CASE category
|
|
WHEN 0 THEN CASE
|
|
WHEN spot_has(subcats, 'z', '{3}') THEN CASE WHEN spot_has(subcats, 'a', '{15}') THEN 6045 WHEN spot_has(subcats, 'a', '{4,6,7,8,9}') THEN 6040
|
|
WHEN spot_has(subcats, 'a', '{12,13}') THEN 6060 ELSE 6070 END
|
|
WHEN spot_has(subcats, 'z', '{1}') THEN CASE WHEN spot_has(subcats, 'a', '{15}') THEN 5045 WHEN spot_has(subcats, 'a', '{4,6,7,8,9}') THEN 5040 ELSE 5030 END
|
|
WHEN spot_has(subcats, 'z', '{2}') OR spot_has(subcats, 'a', '{5,11}') THEN 7020
|
|
WHEN spot_has(subcats, 'z', '{4}') THEN NULL
|
|
ELSE CASE WHEN spot_has(subcats, 'a', '{15}') THEN 2045 WHEN spot_has(subcats, 'a', '{4,6,7,8,9}') THEN 2040 ELSE 2030 END
|
|
END
|
|
WHEN 1 THEN CASE WHEN spot_has(subcats, 'z', '{3}') THEN 3030 WHEN spot_has(subcats, 'a', '{2,4,7,8}') OR spot_has(subcats, 'c', '{8}') THEN 3040 ELSE 3010 END
|
|
WHEN 2 THEN CASE WHEN spot_has(subcats, 'a', '{0,1,2}') THEN 4050 ELSE 1000 END
|
|
WHEN 3 THEN 4000
|
|
END
|
|
$$`)
|
|
this.schema.raw(`
|
|
CREATE OR REPLACE FUNCTION releases_set_category() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
NEW.category := release_category_for(NEW.name, NEW.inner_name, NEW.groups, CASE WHEN TG_OP = 'UPDATE' THEN OLD.category ELSE NEW.category END);
|
|
RETURN NEW;
|
|
END
|
|
$$`)
|
|
// ACCESS EXCLUSIVE for a moment, while the workers write: a few tries instead of a queue behind them.
|
|
this.schema.raw(`
|
|
DO $$
|
|
BEGIN
|
|
FOR attempt IN 1..30 LOOP
|
|
BEGIN
|
|
PERFORM set_config('lock_timeout', '5s', true);
|
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'releases' AND column_name = 'category' AND is_generated = 'ALWAYS') THEN
|
|
ALTER TABLE releases ALTER COLUMN category DROP EXPRESSION;
|
|
END IF;
|
|
DROP TRIGGER IF EXISTS releases_category ON releases;
|
|
CREATE TRIGGER releases_category BEFORE INSERT OR UPDATE OF name, inner_name, groups ON releases
|
|
FOR EACH ROW EXECUTE FUNCTION releases_set_category();
|
|
RETURN;
|
|
EXCEPTION WHEN lock_not_available THEN
|
|
PERFORM pg_sleep(2);
|
|
END;
|
|
END LOOP;
|
|
RAISE EXCEPTION 'releases: no lock for the category trigger in 30 tries';
|
|
END
|
|
$$`)
|
|
}
|
|
|
|
async down() {
|
|
this.schema.raw(`DROP TRIGGER IF EXISTS releases_category ON releases`)
|
|
this.schema.raw(`DROP FUNCTION IF EXISTS releases_set_category()`)
|
|
this.schema.raw(`DROP FUNCTION IF EXISTS spot_category(integer, jsonb)`)
|
|
this.schema.raw(`DROP FUNCTION IF EXISTS spot_has(jsonb, text, integer[])`)
|
|
this.schema.raw(`DROP FUNCTION IF EXISTS release_category_for(text, text, jsonb, integer)`)
|
|
this.schema.raw(`DROP FUNCTION IF EXISTS release_group_category(jsonb)`)
|
|
}
|
|
}
|