diff --git a/config/default.js b/config/default.js index 28d39835a..b5c4f3636 100755 --- a/config/default.js +++ b/config/default.js @@ -38,7 +38,7 @@ module.exports = { login: true, signup: true, usernameLength: [2, 24], - usernamePattern: /^[a-zA-Z0-9_-]$/, + usernamePattern: /^[a-zA-Z0-9_-]+$/, }, exclude: { channels: [ diff --git a/migrations/20190325001339_releases.js b/migrations/20190325001339_releases.js index 336c1118a..a7451343b 100755 --- a/migrations/20190325001339_releases.js +++ b/migrations/20190325001339_releases.js @@ -64,6 +64,7 @@ exports.up = (knex) => Promise.resolve() table.boolean('independent') .defaultTo(false); + table.boolean('showcased'); table.boolean('visible') .defaultTo(true); @@ -651,6 +652,8 @@ exports.up = (knex) => Promise.resolve() table.integer('duration') .unsigned(); + table.specificType('qualities', 'text[]'); + table.boolean('deep'); table.text('deep_url', 1000); @@ -804,6 +807,8 @@ exports.up = (knex) => Promise.resolve() table.text('original_tag'); table.unique(['tag_id', 'release_id']); + table.index('tag_id'); + table.index('release_id'); })) .then(() => knex.schema.createTable('releases_search', (table) => { table.integer('release_id', 16) @@ -873,6 +878,8 @@ exports.up = (knex) => Promise.resolve() table.datetime('created_at') .defaultTo(knex.fn.now()); + + table.index('scene_id'); })) .then(() => knex.schema.createTable('movies_covers', (table) => { table.integer('movie_id', 16) @@ -916,12 +923,151 @@ exports.up = (knex) => Promise.resolve() table.unique('movie_id'); })) + .then(() => knex.schema.createTable('movies_photos', (table) => { + table.integer('movie_id', 16) + .notNullable() + .references('id') + .inTable('movies') + .onDelete('cascade'); + + table.text('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['movie_id', 'media_id']); + })) .then(() => knex.schema.createTable('movies_search', (table) => { table.integer('movie_id', 16) .references('id') .inTable('movies') .onDelete('cascade'); })) + .then(() => knex.schema.createTable('series', (table) => { + table.increments('id', 16); + + table.integer('entity_id', 12) + .references('id') + .inTable('entities') + .notNullable(); + + table.integer('studio_id', 12) + .references('id') + .inTable('entities'); + + table.text('entry_id'); + table.unique(['entity_id', 'entry_id']); + + table.text('url', 1000); + table.text('title'); + table.text('slug'); + + table.timestamp('date'); + table.index('date'); + + table.enum('date_precision', ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']) + .defaultTo('day'); + + table.text('description'); + + table.boolean('deep'); + table.text('deep_url', 1000); + + table.text('comment'); + + table.integer('created_batch_id', 12) + .references('id') + .inTable('batches') + .onDelete('cascade'); + + table.integer('updated_batch_id', 12) + .references('id') + .inTable('batches') + .onDelete('cascade'); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('series_scenes', (table) => { + table.integer('serie_id', 16) + .notNullable() + .references('id') + .inTable('series') + .onDelete('cascade'); + + table.integer('scene_id', 16) + .notNullable() + .references('id') + .inTable('releases') + .onDelete('cascade'); + + table.unique(['serie_id', 'scene_id']); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('series_trailers', (table) => { + table.integer('serie_id', 16) + .unique() + .notNullable() + .references('id') + .inTable('series') + .onDelete('cascade'); + + table.text('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + })) + .then(() => knex.schema.createTable('series_posters', (table) => { + table.integer('serie_id', 16) + .notNullable() + .references('id') + .inTable('series') + .onDelete('cascade'); + + table.text('media_id', 21) + .notNullable() + .references('id') + .inTable('media') + .onDelete('cascade'); + + table.unique('serie_id'); + })) + .then(() => knex.schema.createTable('series_covers', (table) => { + table.integer('serie_id', 16) + .notNullable() + .references('id') + .inTable('series') + .onDelete('cascade'); + + table.text('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['serie_id', 'media_id']); + })) + .then(() => knex.schema.createTable('series_photos', (table) => { + table.integer('serie_id', 16) + .notNullable() + .references('id') + .inTable('series') + .onDelete('cascade'); + + table.text('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['serie_id', 'media_id']); + })) + .then(() => knex.schema.createTable('series_search', (table) => { + table.integer('serie_id', 16) + .references('id') + .inTable('series') + .onDelete('cascade'); + })) .then(() => knex.schema.createTable('chapters', (table) => { table.increments('id', 16); @@ -1062,6 +1208,8 @@ exports.up = (knex) => Promise.resolve() table.datetime('created_at') .notNullable() .defaultTo(knex.fn.now()); + + table.datetime('last_login'); })) .then(() => knex.schema.createTable('stashes', (table) => { table.increments('id'); @@ -1088,7 +1236,12 @@ exports.up = (knex) => Promise.resolve() table.datetime('created_at') .notNullable() .defaultTo(knex.fn.now()); + + table.unique(['user_id', 'slug']); })) + .then(() => knex.raw(` + CREATE UNIQUE INDEX unique_primary ON stashes (user_id, "primary") WHERE ("primary" = TRUE); + `)) .then(() => knex.schema.createTable('stashes_scenes', (table) => { table.integer('stash_id') .notNullable() @@ -1152,6 +1305,27 @@ exports.up = (knex) => Promise.resolve() .notNullable() .defaultTo(knex.fn.now()); })) + .then(() => knex.schema.createTable('stashes_series', (table) => { + table.integer('stash_id') + .notNullable() + .references('id') + .inTable('stashes') + .onDelete('cascade'); + + table.integer('serie_id') + .notNullable() + .references('id') + .inTable('series') + .onDelete('cascade'); + + table.unique(['stash_id', 'serie_id']); + + table.string('comment'); + + table.datetime('created_at') + .notNullable() + .defaultTo(knex.fn.now()); + })) .then(() => knex.schema.createTable('alerts', (table) => { table.increments('id'); @@ -1329,6 +1503,9 @@ exports.up = (knex) => Promise.resolve() .notNullable() .defaultTo(knex.fn.now()); })) + .then(() => knex.raw(` + ALTER TABLE banners ADD COLUMN ratio numeric GENERATED ALWAYS AS (ROUND(width::decimal/ height::decimal, 2)) STORED; + `)) .then(() => knex.schema.createTable('banners_tags', (table) => { table.increments('id'); @@ -1372,12 +1549,32 @@ exports.up = (knex) => Promise.resolve() .notNullable() .defaultTo(knex.fn.now()); })) + .then(() => knex.schema.createTable('random_campaign', (table) => { + table.text('banner_id') + .references('id') + .inTable('banners'); + + table.text('url'); + + table.integer('entity_id') + .references('id') + .inTable('entities'); + + table.string('affiliate_id') + .references('id') + .inTable('affiliates'); + + table.integer('parent_id') + .references('id') + .inTable('entities'); + })) // SEARCH AND SORT .then(() => { // eslint-disable-line arrow-body-style // allow vim fold return knex.raw(` ALTER TABLE releases_search ADD COLUMN document tsvector; ALTER TABLE movies_search ADD COLUMN document tsvector; + ALTER TABLE series_search ADD COLUMN document tsvector; /* allow scenes without dates to be mixed inbetween scenes with dates */ ALTER TABLE releases @@ -1401,6 +1598,8 @@ exports.up = (knex) => Promise.resolve() CREATE UNIQUE INDEX movies_search_unique ON movies_search (movie_id); CREATE INDEX releases_search_index ON releases_search USING GIN (document); CREATE INDEX movies_search_index ON movies_search USING GIN (document); + CREATE UNIQUE INDEX series_search_unique ON series_search (serie_id); + CREATE INDEX series_search_index ON series_search USING GIN (document); `); }) // FUNCTIONS @@ -1421,15 +1620,13 @@ exports.up = (knex) => Promise.resolve() CREATE TABLE movies_search_results (movie_id integer, rank real, FOREIGN KEY (movie_id) REFERENCES movies (id)); CREATE FUNCTION search_releases(query text) RETURNS SETOF releases_search_results AS $$ - SELECT releases.id, ranks.rank FROM ( - SELECT - releases_search.release_id, - ts_rank(releases_search.document, to_tsquery('english', array_to_string(array(SELECT * FROM regexp_matches(query, '[A-Za-zÀ-ÖØ-öø-ÿ0-9]+', 'g')), '|'))) AS rank + SELECT results.release_id, ts_rank(results.document::tsvector, curate_search_query(query)) as rank + FROM ( + SELECT releases_search.release_id, document FROM releases_search - ) ranks - LEFT JOIN releases ON releases.id = ranks.release_id - WHERE ranks.rank > 0 - ORDER BY ranks.rank DESC; + WHERE document::tsvector @@ curate_search_query(query) + ) AS results + ORDER BY rank DESC; $$ LANGUAGE SQL STABLE; CREATE FUNCTION search_movies(query text) RETURNS SETOF movies_search_results AS $$ @@ -1513,6 +1710,52 @@ exports.up = (knex) => Promise.resolve() ORDER BY actors.name; $$ LANGUAGE SQL STABLE; + CREATE FUNCTION entities_scenes(entity entities) RETURNS SETOF releases AS $$ + WITH RECURSIVE children AS ( + SELECT entities.id + FROM entities + WHERE entities.id = entity.id + + UNION ALL + + SELECT entities.id + FROM entities + INNER JOIN children ON children.id = entities.parent_id + ) + + SELECT releases FROM releases + INNER JOIN children ON children.id = releases.entity_id + + UNION + + SELECT releases FROM releases + INNER JOIN children ON children.id = releases.studio_id; + $$ LANGUAGE SQL STABLE; + + CREATE OR REPLACE FUNCTION entities_scene_total(entity entities) RETURNS bigint AS $$ + SELECT COUNT(id) + FROM releases + WHERE releases.entity_id = entity.id; + $$ LANGUAGE SQL STABLE; + + CREATE FUNCTION entities_scene_tags(entity entities, selectable_tags text[]) RETURNS SETOF tags AS $$ + SELECT tags.* + FROM releases + LEFT JOIN + releases_tags ON releases_tags.release_id = releases.id + LEFT JOIN + tags ON tags.id = releases_tags.tag_id + WHERE + releases.entity_id = entity.id + AND + CASE WHEN array_length(selectable_tags, 1) IS NOT NULL + THEN tags.slug = ANY(selectable_tags) + ELSE true + END + GROUP BY tags.id + ORDER BY tags.name; + $$ LANGUAGE SQL STABLE; + /* GraphQL/Postgraphile 'every' applies to the data, will only include scenes for which every assigned tag is selected, instead of what we want; scenes with every selected tag, but possibly also some others */ CREATE FUNCTION actors_scenes(actor actors, selected_tags text[], mode text DEFAULT 'all') RETURNS SETOF releases AS $$ @@ -1589,7 +1832,7 @@ exports.up = (knex) => Promise.resolve() ORDER BY tags.priority DESC $$ LANGUAGE SQL STABLE; - CREATE FUNCTION movies_photos(movie movies) RETURNS SETOF media AS $$ + CREATE FUNCTION movies_scenes_photos(movie movies) RETURNS SETOF media AS $$ SELECT media.* FROM movies_scenes LEFT JOIN @@ -1607,15 +1850,111 @@ exports.up = (knex) => Promise.resolve() SELECT EXISTS(SELECT true WHERE (SELECT id FROM batches ORDER BY created_at DESC LIMIT 1) = release.created_batch_id); $$ LANGUAGE sql STABLE; - CREATE FUNCTION banners_ratio(banner banners) RETURNS numeric AS $$ - SELECT ROUND(banner.width::decimal / banner.height::decimal, 2); + CREATE FUNCTION series_actors(serie series) RETURNS SETOF actors AS $$ + SELECT actors.* + FROM series_scenes + LEFT JOIN + releases ON releases.id = series_scenes.scene_id + LEFT JOIN + releases_actors ON releases_actors.release_id = releases.id + LEFT JOIN + actors ON actors.id = releases_actors.actor_id + WHERE series_scenes.serie_id = serie.id + AND actors.id IS NOT NULL + GROUP BY actors.id + ORDER BY actors.name, actors.gender $$ LANGUAGE SQL STABLE; - CREATE FUNCTION get_random_campaign() RETURNS SETOF campaigns AS $$ - SELECT * FROM campaigns - ORDER BY random() + CREATE FUNCTION series_tags(serie series) RETURNS SETOF tags AS $$ + SELECT tags.* + FROM series_scenes + LEFT JOIN + releases ON releases.id = series_scenes.scene_id + LEFT JOIN + releases_tags ON releases_tags.release_id = releases.id + LEFT JOIN + tags ON tags.id = releases_tags.tag_id + WHERE series_scenes.serie_id = serie.id + AND tags.id IS NOT NULL + GROUP BY tags.id + ORDER BY tags.priority DESC + $$ LANGUAGE SQL STABLE; + + CREATE FUNCTION series_scenes_photos(serie series) RETURNS SETOF media AS $$ + SELECT media.* + FROM series_scenes + LEFT JOIN + releases ON releases.id = series_scenes.scene_id + INNER JOIN + releases_photos ON releases_photos.release_id = releases.id + LEFT JOIN + media ON media.id = releases_photos.media_id + WHERE series_scenes.serie_id = serie.id + GROUP BY media.id + ORDER BY media.index ASC + $$ LANGUAGE SQL STABLE; + + CREATE FUNCTION get_random_campaign(min_ratio decimal default 0, max_ratio decimal default 1000.0) RETURNS random_campaign AS $$ + SELECT * FROM ( + SELECT DISTINCT ON (CASE WHEN parent_id IS NOT NULL THEN parent_id ELSE entity_id END) + banner_id, url, entity_id, affiliate_id, parent_id + FROM ( + SELECT + campaigns.*, entities.parent_id as parent_id + FROM campaigns + LEFT JOIN entities ON entities.id = campaigns.entity_id + LEFT JOIN banners ON banners.id = campaigns.banner_id + WHERE banner_id IS NOT NULL + AND ratio >= min_ratio + AND ratio <= max_ratio + ORDER BY RANDOM() + ) random_campaigns + ) random_banners + ORDER BY RANDOM() LIMIT 1; - $$ LANGUAGE sql STABLE; + $$ LANGUAGE SQL STABLE; + `); + }) + // VIEWS AND COMMENTS + .then(() => { // eslint-disable-line arrow-body-style + // allow vim fold + return knex.raw(` + CREATE MATERIALIZED VIEW releases_not_showcased AS ( + SELECT releases.id AS release_id FROM releases + LEFT JOIN entities AS channels ON channels.id = releases.entity_id + LEFT JOIN entities AS studios ON studios.id = releases.studio_id + LEFT JOIN entities AS networks ON networks.id = channels.parent_id + WHERE (studios.showcased = false) + OR (channels.showcased = false AND studios.showcased IS NOT true) + OR (networks.showcased = false AND channels.showcased IS NOT true AND studios.showcased IS NOT true) + ); + + CREATE UNIQUE INDEX ON releases_not_showcased (release_id); + COMMENT ON MATERIALIZED VIEW releases_not_showcased IS E'@foreignKey (release_id) references releases (id)'; + + COMMENT ON COLUMN users.password IS E'@omit'; + COMMENT ON COLUMN users.email IS E'@omit'; + COMMENT ON COLUMN users.email_verified IS E'@omit'; + COMMENT ON COLUMN users.abilities IS E'@omit'; + + COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many'; + COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many'; + COMMENT ON COLUMN actors.penis_length IS E'@omit read,update,create,delete,all,many'; + COMMENT ON COLUMN actors.penis_girth IS E'@omit read,update,create,delete,all,many'; + + COMMENT ON FUNCTION entities_scenes IS E'@sortable'; + + COMMENT ON FUNCTION actors_tags IS E'@sortable'; + COMMENT ON FUNCTION actors_channels IS E'@sortable'; + COMMENT ON FUNCTION actors_actors IS E'@sortable'; + COMMENT ON FUNCTION actors_scenes IS E'@sortable'; + COMMENT ON FUNCTION tags_scenes IS E'@sortable'; + + COMMENT ON FUNCTION search_releases IS E'@sortable'; + COMMENT ON FUNCTION search_entities IS E'@sortable'; + COMMENT ON FUNCTION search_actors IS E'@sortable'; + COMMENT ON FUNCTION search_movies IS E'@sortable'; + COMMENT ON FUNCTION search_tags IS E'@sortable'; `); }) // POLICIES @@ -1632,6 +1971,7 @@ exports.up = (knex) => Promise.resolve() ALTER TABLE stashes_scenes ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes_movies ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes_actors ENABLE ROW LEVEL SECURITY; + ALTER TABLE stashes_series ENABLE ROW LEVEL SECURITY; CREATE POLICY stashes_policy_select ON stashes FOR SELECT USING (stashes.public OR stashes.user_id = current_user_id()); CREATE POLICY stashes_policy_update ON stashes FOR UPDATE USING (stashes.public OR stashes.user_id = current_user_id()); @@ -1662,6 +2002,14 @@ exports.up = (knex) => Promise.resolve() AND (stashes.user_id = current_user_id() OR stashes.public) )); + CREATE POLICY stashes_policy ON stashes_series + USING (EXISTS ( + SELECT * + FROM stashes + WHERE stashes.id = stashes_series.stash_id + AND (stashes.user_id = current_user_id() OR stashes.public) + )); + ALTER TABLE alerts ENABLE ROW LEVEL SECURITY; ALTER TABLE alerts_tags ENABLE ROW LEVEL SECURITY; ALTER TABLE alerts_scenes ENABLE ROW LEVEL SECURITY; @@ -1729,33 +2077,6 @@ exports.up = (knex) => Promise.resolve() `, { visitor: knex.raw(config.database.query.user), }); - }) - // VIEWS AND COMMENTS - .then(() => { // eslint-disable-line arrow-body-style - // allow vim fold - return knex.raw(` - COMMENT ON COLUMN users.password IS E'@omit'; - COMMENT ON COLUMN users.email IS E'@omit'; - COMMENT ON COLUMN users.email_verified IS E'@omit'; - COMMENT ON COLUMN users.abilities IS E'@omit'; - - COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many'; - COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many'; - COMMENT ON COLUMN actors.penis_length IS E'@omit read,update,create,delete,all,many'; - COMMENT ON COLUMN actors.penis_girth IS E'@omit read,update,create,delete,all,many'; - - COMMENT ON FUNCTION actors_tags IS E'@sortable'; - COMMENT ON FUNCTION actors_channels IS E'@sortable'; - COMMENT ON FUNCTION actors_actors IS E'@sortable'; - COMMENT ON FUNCTION actors_scenes IS E'@sortable'; - COMMENT ON FUNCTION tags_scenes IS E'@sortable'; - - COMMENT ON FUNCTION search_releases IS E'@sortable'; - COMMENT ON FUNCTION search_entities IS E'@sortable'; - COMMENT ON FUNCTION search_actors IS E'@sortable'; - COMMENT ON FUNCTION search_movies IS E'@sortable'; - COMMENT ON FUNCTION search_tags IS E'@sortable'; - `); }); exports.down = (knex) => { // eslint-disable-line arrow-body-style @@ -1776,8 +2097,17 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style DROP TABLE IF EXISTS movies_scenes CASCADE; DROP TABLE IF EXISTS movies_covers CASCADE; DROP TABLE IF EXISTS movies_posters CASCADE; + DROP TABLE IF EXISTS movies_photos CASCADE; DROP TABLE IF EXISTS movies_trailers CASCADE; + DROP TABLE IF EXISTS stashes_series CASCADE; + DROP TABLE IF EXISTS series_scenes CASCADE; + DROP TABLE IF EXISTS series_trailers CASCADE; + DROP TABLE IF EXISTS series_posters CASCADE; + DROP TABLE IF EXISTS series_covers CASCADE; + DROP TABLE IF EXISTS series_photos CASCADE; + DROP TABLE IF EXISTS series_search CASCADE; + DROP TABLE IF EXISTS clips_tags CASCADE; DROP TABLE IF EXISTS clips_posters CASCADE; DROP TABLE IF EXISTS clips_photos CASCADE; @@ -1789,6 +2119,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style DROP TABLE IF EXISTS banners_tags CASCADE; DROP TABLE IF EXISTS banners CASCADE; DROP TABLE IF EXISTS campaigns CASCADE; + DROP TABLE IF EXISTS random_campaign CASCADE; DROP TABLE IF EXISTS affiliates CASCADE; DROP TABLE IF EXISTS batches CASCADE; @@ -1812,6 +2143,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style DROP TABLE IF EXISTS movies CASCADE; DROP TABLE IF EXISTS clips CASCADE; DROP TABLE IF EXISTS chapters CASCADE; + DROP TABLE IF EXISTS series CASCADE; DROP TABLE IF EXISTS releases CASCADE; DROP TABLE IF EXISTS actors CASCADE; DROP TABLE IF EXISTS tags CASCADE; @@ -1853,6 +2185,9 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style DROP FUNCTION IF EXISTS get_random_sfw_media_id; DROP FUNCTION IF EXISTS releases_is_new; + DROP FUNCTION IF EXISTS entities_scenes; + DROP FUNCTION IF EXISTS entities_scene_total; + DROP FUNCTION IF EXISTS entities_scene_tags; DROP FUNCTION IF EXISTS actors_tags; DROP FUNCTION IF EXISTS actors_channels; DROP FUNCTION IF EXISTS actors_actors; @@ -1860,7 +2195,11 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style DROP FUNCTION IF EXISTS movies_actors; DROP FUNCTION IF EXISTS movies_tags; - DROP FUNCTION IF EXISTS movies_photos; + DROP FUNCTION IF EXISTS movies_scenes_photos; + + DROP FUNCTION IF EXISTS series_actors; + DROP FUNCTION IF EXISTS series_tags; + DROP FUNCTION IF EXISTS series_scenes_photos; DROP POLICY IF EXISTS stashes_policy ON stashes; DROP POLICY IF EXISTS stashes_policy ON stashes_scenes; diff --git a/migrations/20211030162955_entity_scenes.js b/migrations/20211030162955_entity_scenes.js deleted file mode 100755 index ef0d442c6..000000000 --- a/migrations/20211030162955_entity_scenes.js +++ /dev/null @@ -1,29 +0,0 @@ -exports.up = async (knex) => knex.raw(` - CREATE FUNCTION entities_scenes(entity entities) RETURNS SETOF releases AS $$ - WITH RECURSIVE children AS ( - SELECT entities.id - FROM entities - WHERE entities.id = entity.id - - UNION ALL - - SELECT entities.id - FROM entities - INNER JOIN children ON children.id = entities.parent_id - ) - - SELECT releases FROM releases - INNER JOIN children ON children.id = releases.entity_id - - UNION - - SELECT releases FROM releases - INNER JOIN children ON children.id = releases.studio_id; - $$ LANGUAGE SQL STABLE; - - COMMENT ON FUNCTION entities_scenes IS E'@sortable'; -`); - -exports.down = async (knex) => knex.raw(` - DROP FUNCTION IF EXISTS entities_scenes; -`); diff --git a/migrations/20220123234131_scene_indexes.js b/migrations/20220123234131_scene_indexes.js deleted file mode 100755 index 07d6b7cb3..000000000 --- a/migrations/20220123234131_scene_indexes.js +++ /dev/null @@ -1,15 +0,0 @@ -exports.up = async (knex) => Promise.resolve() - .then(() => knex.schema.alterTable('releases_tags', (table) => { - table.index('release_id'); - })) - .then(() => knex.schema.alterTable('movies_scenes', (table) => { - table.index('scene_id'); - })); - -exports.down = async (knex) => Promise.resolve() - .then(() => knex.schema.alterTable('releases_tags', (table) => { - table.dropIndex('release_id'); - })) - .then(() => knex.schema.alterTable('movies_scenes', (table) => { - table.dropIndex('scene_id'); - })); diff --git a/migrations/20220126132955_scene_counts.js b/migrations/20220126132955_scene_counts.js deleted file mode 100755 index 310585cbb..000000000 --- a/migrations/20220126132955_scene_counts.js +++ /dev/null @@ -1,11 +0,0 @@ -exports.up = async (knex) => knex.raw(` - CREATE OR REPLACE FUNCTION entities_scene_total(entity entities) RETURNS bigint AS $$ - SELECT COUNT(id) - FROM releases - WHERE releases.entity_id = entity.id; - $$ LANGUAGE SQL STABLE; -`); - -exports.down = async (knex) => knex.raw(` - DROP FUNCTION IF EXISTS entities_scene_total; -`); diff --git a/migrations/20220227215315_entity_filters.js b/migrations/20220227215315_entity_filters.js deleted file mode 100755 index 4696e9c3a..000000000 --- a/migrations/20220227215315_entity_filters.js +++ /dev/null @@ -1,23 +0,0 @@ -exports.up = async (knex) => knex.raw(` - CREATE FUNCTION entities_scene_tags(entity entities, selectable_tags text[]) RETURNS SETOF tags AS $$ - SELECT tags.* - FROM releases - LEFT JOIN - releases_tags ON releases_tags.release_id = releases.id - LEFT JOIN - tags ON tags.id = releases_tags.tag_id - WHERE - releases.entity_id = entity.id - AND - CASE WHEN array_length(selectable_tags, 1) IS NOT NULL - THEN tags.slug = ANY(selectable_tags) - ELSE true - END - GROUP BY tags.id - ORDER BY tags.name; - $$ LANGUAGE SQL STABLE; -`); - -exports.down = async (knex) => knex.raw(` - DROP FUNCTION IF EXISTS entities_scene_tags; -`); diff --git a/migrations/20220304233846_series.js b/migrations/20220304233846_series.js deleted file mode 100755 index db4411995..000000000 --- a/migrations/20220304233846_series.js +++ /dev/null @@ -1,215 +0,0 @@ -const config = require('config'); - -exports.up = async (knex) => Promise.resolve() - .then(() => knex.schema.createTable('series', (table) => { - table.increments('id', 16); - - table.integer('entity_id', 12) - .references('id') - .inTable('entities') - .notNullable(); - - table.integer('studio_id', 12) - .references('id') - .inTable('entities'); - - table.text('entry_id'); - table.unique(['entity_id', 'entry_id']); - - table.text('url', 1000); - table.text('title'); - table.text('slug'); - - table.timestamp('date'); - table.index('date'); - - table.enum('date_precision', ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']) - .defaultTo('day'); - - table.text('description'); - - table.boolean('deep'); - table.text('deep_url', 1000); - - table.text('comment'); - - table.integer('created_batch_id', 12) - .references('id') - .inTable('batches') - .onDelete('cascade'); - - table.integer('updated_batch_id', 12) - .references('id') - .inTable('batches') - .onDelete('cascade'); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('series_scenes', (table) => { - table.integer('serie_id', 16) - .notNullable() - .references('id') - .inTable('series') - .onDelete('cascade'); - - table.integer('scene_id', 16) - .notNullable() - .references('id') - .inTable('releases') - .onDelete('cascade'); - - table.unique(['serie_id', 'scene_id']); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('series_trailers', (table) => { - table.integer('serie_id', 16) - .unique() - .notNullable() - .references('id') - .inTable('series') - .onDelete('cascade'); - - table.text('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - })) - .then(() => knex.schema.createTable('series_posters', (table) => { - table.integer('serie_id', 16) - .notNullable() - .references('id') - .inTable('series') - .onDelete('cascade'); - - table.text('media_id', 21) - .notNullable() - .references('id') - .inTable('media') - .onDelete('cascade'); - - table.unique('serie_id'); - })) - .then(() => knex.schema.createTable('series_covers', (table) => { - table.integer('serie_id', 16) - .notNullable() - .references('id') - .inTable('series') - .onDelete('cascade'); - - table.text('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['serie_id', 'media_id']); - })) - .then(() => knex.schema.createTable('series_search', (table) => { - table.integer('serie_id', 16) - .references('id') - .inTable('series') - .onDelete('cascade'); - })) - .then(() => knex.schema.createTable('stashes_series', (table) => { - table.integer('stash_id') - .notNullable() - .references('id') - .inTable('stashes') - .onDelete('cascade'); - - table.integer('serie_id') - .notNullable() - .references('id') - .inTable('series') - .onDelete('cascade'); - - table.unique(['stash_id', 'serie_id']); - - table.string('comment'); - - table.datetime('created_at') - .notNullable() - .defaultTo(knex.fn.now()); - })) - .then(() => knex.raw(` - ALTER TABLE series_search ADD COLUMN document tsvector; - - CREATE UNIQUE INDEX series_search_unique ON series_search (serie_id); - CREATE INDEX series_search_index ON series_search USING GIN (document); - - CREATE FUNCTION series_actors(serie series) RETURNS SETOF actors AS $$ - SELECT actors.* - FROM series_scenes - LEFT JOIN - releases ON releases.id = series_scenes.scene_id - LEFT JOIN - releases_actors ON releases_actors.release_id = releases.id - LEFT JOIN - actors ON actors.id = releases_actors.actor_id - WHERE series_scenes.serie_id = serie.id - AND actors.id IS NOT NULL - GROUP BY actors.id - ORDER BY actors.name, actors.gender - $$ LANGUAGE SQL STABLE; - - CREATE FUNCTION series_tags(serie series) RETURNS SETOF tags AS $$ - SELECT tags.* - FROM series_scenes - LEFT JOIN - releases ON releases.id = series_scenes.scene_id - LEFT JOIN - releases_tags ON releases_tags.release_id = releases.id - LEFT JOIN - tags ON tags.id = releases_tags.tag_id - WHERE series_scenes.serie_id = serie.id - AND tags.id IS NOT NULL - GROUP BY tags.id - ORDER BY tags.priority DESC - $$ LANGUAGE SQL STABLE; - - CREATE FUNCTION series_photos(serie series) RETURNS SETOF media AS $$ - SELECT media.* - FROM series_scenes - LEFT JOIN - releases ON releases.id = series_scenes.scene_id - INNER JOIN - releases_photos ON releases_photos.release_id = releases.id - LEFT JOIN - media ON media.id = releases_photos.media_id - WHERE series_scenes.serie_id = serie.id - GROUP BY media.id - ORDER BY media.index ASC - $$ LANGUAGE SQL STABLE; - - GRANT ALL ON ALL TABLES IN SCHEMA public TO :visitor; - GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :visitor; - - ALTER TABLE stashes_series ENABLE ROW LEVEL SECURITY; - - CREATE POLICY stashes_policy ON stashes_series - USING (EXISTS ( - SELECT * - FROM stashes - WHERE stashes.id = stashes_series.stash_id - AND (stashes.user_id = current_user_id() OR stashes.public) - )); - `, { - visitor: knex.raw(config.database.query.user), - })); - -exports.down = async (knex) => Promise.resolve() - .then(() => knex.raw(` - DROP FUNCTION IF EXISTS series_actors; - DROP FUNCTION IF EXISTS series_tags; - DROP FUNCTION IF EXISTS series_photos; - - DROP TABLE IF EXISTS stashes_series CASCADE; - DROP TABLE IF EXISTS series_scenes CASCADE; - DROP TABLE IF EXISTS series_trailers CASCADE; - DROP TABLE IF EXISTS series_posters CASCADE; - DROP TABLE IF EXISTS series_covers CASCADE; - DROP TABLE IF EXISTS series_search CASCADE; - DROP TABLE IF EXISTS series CASCADE; - `)); diff --git a/migrations/20220327230125_movie_photos.js b/migrations/20220327230125_movie_photos.js deleted file mode 100755 index 41e09bc45..000000000 --- a/migrations/20220327230125_movie_photos.js +++ /dev/null @@ -1,49 +0,0 @@ -const config = require('config'); - -exports.up = async (knex) => Promise.resolve() - .then(() => knex.raw(` - ALTER FUNCTION movies_photos(movie movies) RENAME TO movies_scenes_photos; - ALTER FUNCTION series_photos(serie series) RENAME TO series_scenes_photos; - `)) - .then(() => knex.schema.createTable('movies_photos', (table) => { - table.integer('movie_id', 16) - .notNullable() - .references('id') - .inTable('movies') - .onDelete('cascade'); - - table.text('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['movie_id', 'media_id']); - })) - .then(() => knex.schema.createTable('series_photos', (table) => { - table.integer('serie_id', 16) - .notNullable() - .references('id') - .inTable('series') - .onDelete('cascade'); - - table.text('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['serie_id', 'media_id']); - })) - .then(() => knex.raw(` - GRANT ALL ON ALL TABLES IN SCHEMA public TO :visitor; - GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :visitor; - `, { - visitor: knex.raw(config.database.query.user), - })); - -exports.down = async (knex) => knex.raw(` - DROP TABLE IF EXISTS movies_photos CASCADE; - DROP TABLE IF EXISTS series_photos CASCADE; - - ALTER FUNCTION movies_scenes_photos(movie movies) RENAME TO movies_photos; - ALTER FUNCTION series_scenes_photos(serie series) RENAME TO series_photos; -`); diff --git a/migrations/20220331135618_qualities.js b/migrations/20220331135618_qualities.js deleted file mode 100755 index 0a8c1650c..000000000 --- a/migrations/20220331135618_qualities.js +++ /dev/null @@ -1,7 +0,0 @@ -exports.up = async (knex) => knex.schema.alterTable('releases', (table) => { - table.specificType('qualities', 'text[]'); -}); - -exports.down = async (knex) => knex.schema.alterTable('releases', (table) => { - table.dropColumn('qualities'); -}); diff --git a/migrations/20220403235645_last_login.js b/migrations/20220403235645_last_login.js deleted file mode 100755 index 34cc2ad15..000000000 --- a/migrations/20220403235645_last_login.js +++ /dev/null @@ -1,7 +0,0 @@ -exports.up = async (knex) => knex.schema.alterTable('users', (table) => { - table.datetime('last_login'); -}); - -exports.down = async (knex) => knex.schema.alterTable('users', (table) => { - table.dropColumn('last_login'); -}); diff --git a/migrations/20220716223422_random_campaign.js b/migrations/20220716223422_random_campaign.js deleted file mode 100755 index 1f6f60753..000000000 --- a/migrations/20220716223422_random_campaign.js +++ /dev/null @@ -1,58 +0,0 @@ -exports.up = async (knex) => Promise.resolve() - .then(() => knex.schema.createTable('random_campaign', (table) => { - table.text('banner_id') - .references('id') - .inTable('banners'); - - table.text('url'); - - table.integer('entity_id') - .references('id') - .inTable('entities'); - - table.string('affiliate_id') - .references('id') - .inTable('affiliates'); - - table.integer('parent_id') - .references('id') - .inTable('entities'); - })) - .then(() => knex.raw(` - ALTER TABLE banners ADD COLUMN ratio numeric GENERATED ALWAYS AS (ROUND(width::decimal/ height::decimal, 2)) STORED; - `)) - .then(() => knex.raw(` - DROP FUNCTION IF EXISTS get_random_campaign; - DROP FUNCTION IF EXISTS banners_ratio; - - CREATE FUNCTION get_random_campaign(min_ratio decimal default 0, max_ratio decimal default 1000.0) RETURNS random_campaign AS $$ - SELECT * FROM ( - SELECT DISTINCT ON (CASE WHEN parent_id IS NOT NULL THEN parent_id ELSE entity_id END) - banner_id, url, entity_id, affiliate_id, parent_id - FROM ( - SELECT - campaigns.*, entities.parent_id as parent_id - FROM campaigns - LEFT JOIN entities ON entities.id = campaigns.entity_id - LEFT JOIN banners ON banners.id = campaigns.banner_id - WHERE banner_id IS NOT NULL - AND ratio >= min_ratio - AND ratio <= max_ratio - ORDER BY RANDOM() - ) random_campaigns - ) random_banners - ORDER BY RANDOM() - LIMIT 1; - $$ LANGUAGE SQL STABLE; - `)); - -exports.down = async (knex) => knex.raw(` - DROP FUNCTION IF EXISTS get_random_campaign; - DROP TABLE IF EXISTS random_campaign; - - ALTER TABLE banners DROP COLUMN ratio; - - CREATE FUNCTION banners_ratio(banner banners) RETURNS numeric AS $$ - SELECT ROUND(banner.width::decimal / banner.height::decimal, 2); - $$ LANGUAGE SQL STABLE; -`); diff --git a/migrations/20221003001803_entity_showcased.js b/migrations/20221003001803_entity_showcased.js deleted file mode 100755 index c817da6a3..000000000 --- a/migrations/20221003001803_entity_showcased.js +++ /dev/null @@ -1,7 +0,0 @@ -exports.up = async (knex) => knex.schema.alterTable('entities', (table) => { - table.boolean('showcased'); -}); - -exports.down = async (knex) => knex.schema.alterTable('entities', (table) => { - table.dropColumn('showcased'); -}); diff --git a/migrations/20230104014437_release_showcased.js b/migrations/20230104014437_release_showcased.js deleted file mode 100644 index d23000a25..000000000 --- a/migrations/20230104014437_release_showcased.js +++ /dev/null @@ -1,22 +0,0 @@ -const config = require('config'); - -exports.up = async (knex) => knex.raw(` - CREATE VIEW releases_not_showcased AS ( - SELECT releases.id AS release_id FROM releases - LEFT JOIN entities AS channels ON channels.id = releases.entity_id - LEFT JOIN entities AS studios ON studios.id = releases.studio_id - LEFT JOIN entities AS networks ON networks .id = entities.parent_id - WHERE (studios.showcased = false) - OR (channel.showcased = false AND studios.showcased IS NOT true) - OR (network.showcased = false AND channel.showcased IS NOT true AND studios.showcased IS NOT true) - ); - - COMMENT ON VIEW releases_not_showcased IS E'@foreignKey (release_id) references releases (id)'; - GRANT SELECT ON releases_not_showcased TO :visitor; -`, { - visitor: knex.raw(config.database.query.user), -}); - -exports.down = async (knex) => knex.raw(` - DROP VIEW IF EXISTS releases_not_showcased; -`); diff --git a/migrations/20230607231459_stash_unique.js b/migrations/20230607231459_stash_unique.js deleted file mode 100644 index 91088418f..000000000 --- a/migrations/20230607231459_stash_unique.js +++ /dev/null @@ -1,19 +0,0 @@ -exports.up = async (knex) => { - await knex.schema.alterTable('stashes', (table) => { - table.unique(['user_id', 'slug']); - }); - - await knex.raw(` - CREATE UNIQUE INDEX unique_primary ON stashes (user_id, "primary") WHERE ("primary" = TRUE); - `); -}; - -exports.down = async (knex) => { - await knex.schema.alterTable('stashes', (table) => { - table.dropUnique(['user_id', 'slug']); - }); - - await knex.raw(` - DROP INDEX unique_primary; - `); -}; diff --git a/migrations/20230612012243_indexes.js b/migrations/20230612012243_indexes.js deleted file mode 100644 index 18d0d0629..000000000 --- a/migrations/20230612012243_indexes.js +++ /dev/null @@ -1,7 +0,0 @@ -exports.up = async (knex) => { - await knex.raw('CREATE INDEX releases_tag_index ON releases_tags(tag_id);'); -}; - -exports.down = async (knex) => { - await knex.raw('DROP INDEX releases_tag_index;'); -}; diff --git a/migrations/20230616000529_search.js b/migrations/20230616000529_search.js deleted file mode 100644 index f9bca5e59..000000000 --- a/migrations/20230616000529_search.js +++ /dev/null @@ -1,38 +0,0 @@ -exports.up = async (knex) => { - await knex.raw(` - CREATE OR REPLACE FUNCTION curate_search_query(query text) RETURNS tsquery AS $$ - SELECT to_tsquery('english', array_to_string(array(SELECT * FROM regexp_matches(query, '[A-Za-zÀ-ÖØ-öø-ÿ0-9]+', 'g')), '|')); - $$ LANGUAGE SQL STABLE; - - CREATE OR REPLACE FUNCTION search_releases(query text) RETURNS SETOF releases_search_results AS $$ - SELECT results.release_id, ts_rank(results.document::tsvector, curate_search_query(query)) as rank - FROM ( - SELECT releases_search.release_id, document - FROM releases_search - WHERE document::tsvector @@ curate_search_query(query) - ) AS results - ORDER BY rank DESC; - $$ LANGUAGE SQL STABLE; - - COMMENT ON FUNCTION search_releases IS E'@sortable'; - `); -}; - -exports.down = async (knex) => { - await knex.raw(` - CREATE OR REPLACE FUNCTION search_releases(query text) RETURNS SETOF releases_search_results AS $$ - SELECT releases.id, ranks.rank FROM ( - SELECT - releases_search.release_id, - ts_rank(releases_search.document, to_tsquery('english', array_to_string(array(SELECT * FROM regexp_matches(query, '[A-Za-zÀ-ÖØ-öø-ÿ0-9]+', 'g')), '|'))) AS rank - FROM releases_search - ) ranks - LEFT JOIN releases ON releases.id = ranks.release_id - WHERE ranks.rank > 0 - ORDER BY ranks.rank DESC; - $$ LANGUAGE SQL STABLE; - - DROP FUNCTION curate_search_query; - COMMENT ON FUNCTION search_releases IS E'@sortable'; - `); -}; diff --git a/migrations/_20220330230122_stats.js b/migrations/_20220330230122_stats.js deleted file mode 100755 index fde6a0cd2..000000000 --- a/migrations/_20220330230122_stats.js +++ /dev/null @@ -1,25 +0,0 @@ -exports.up = async (knex) => knex.raw(` - CREATE MATERIALIZED VIEW entities_stats - AS - WITH RECURSIVE relations AS ( - SELECT entities.id, entities.parent_id, count(releases.id) AS releases_count, count(releases.id) AS total_count - FROM entities - LEFT JOIN releases ON releases.entity_id = entities.id - GROUP BY entities.id - - UNION ALL - - SELECT entities.id AS entity_id, count(releases.id) AS releases_count, count(releases.id) + relations.total_count AS total_count - FROM entities - INNER JOIN relations ON relations.id = entities.parent_id - LEFT JOIN releases ON releases.entity_id = entities.id - GROUP BY entities.id - ) - - SELECT relations.id AS entity_id, relations.releases_count - FROM relations; -`); - -exports.down = async (knex) => knex.raw(` - DROP MATERIALIZED VIEW entities_stats; -`); diff --git a/public/img/logos/hotwiferio/hotwiferio.png b/public/img/logos/hotwiferio/hotwiferio.png new file mode 100644 index 000000000..cb96ad4e9 Binary files /dev/null and b/public/img/logos/hotwiferio/hotwiferio.png differ diff --git a/public/img/logos/hotwiferio/misc/background.png b/public/img/logos/hotwiferio/misc/background.png new file mode 100644 index 000000000..554e65b6e Binary files /dev/null and b/public/img/logos/hotwiferio/misc/background.png differ diff --git a/public/img/logos/hotwiferio/misc/footer.png b/public/img/logos/hotwiferio/misc/footer.png new file mode 100644 index 000000000..9a70be169 Binary files /dev/null and b/public/img/logos/hotwiferio/misc/footer.png differ diff --git a/src/store-releases.js b/src/store-releases.js index a38666075..46ad7e8cf 100755 --- a/src/store-releases.js +++ b/src/store-releases.js @@ -269,6 +269,8 @@ async function updateSceneSearch(releaseIds) { if (documents.rows?.length > 0) { await bulkInsert('releases_search', documents.rows, ['release_id']); } + + await knex.raw('REFRESH MATERIALIZED VIEW CONCURRENTLY releases_not_showcased;'); } async function storeChapters(releases) {