Compare commits

..

251 Commits

Author SHA1 Message Date
DebaucheryLibrarian 90aa29d2d4 1.233.0 2023-11-24 02:10:06 +01:00
DebaucheryLibrarian 0369446681 Added AND/OR toggle to alerts. 2023-11-24 02:10:03 +01:00
DebaucheryLibrarian 238dce78b5 Refactored alerts to use application code, added regex. Updated Jules Jordan for the Ass Factory relaunch. 2023-11-24 01:29:22 +01:00
DebaucheryLibrarian 124ff3f5e3 1.232.2 2023-11-09 01:10:08 +01:00
DebaucheryLibrarian 1cf06a8b49 Fixed Vixen upcoming. 2023-11-09 01:10:05 +01:00
DebaucheryLibrarian a8bdb7ab59 1.232.1 2023-11-08 03:41:44 +01:00
DebaucheryLibrarian 08fd69af39 Added photo_count to movies table. 2023-11-08 03:41:41 +01:00
DebaucheryLibrarian 560ff103ce 1.232.0 2023-11-07 04:46:48 +01:00
DebaucheryLibrarian edc724b475 Storing photo counts. 2023-11-07 04:46:45 +01:00
DebaucheryLibrarian ac904c849d Fixed The Flourish POV URL. 2023-11-07 04:39:14 +01:00
DebaucheryLibrarian 5481b6e0a6 1.231.31 2023-11-07 04:29:38 +01:00
DebaucheryLibrarian eba96df631 Added The Flourish, adapted Arch Angel scraper. 2023-11-07 04:29:35 +01:00
DebaucheryLibrarian 7caa325c5f 1.231.30 2023-11-07 03:06:42 +01:00
DebaucheryLibrarian e20641e101 Fixed Teen Mega World scraper. 2023-11-07 03:06:39 +01:00
DebaucheryLibrarian 8f81f71802 1.231.29 2023-09-24 04:29:46 +02:00
DebaucheryLibrarian e7a4ccecf3 Renamed Natasha Teen Productions to Natasha Teen Films. 2023-09-24 04:29:43 +02:00
DebaucheryLibrarian ec33a8b5a9 1.231.28 2023-09-17 21:59:18 +02:00
DebaucheryLibrarian 4a9aa62831 Changed inadvertent click prevention in actor tile scrolling for improved native interactions. 2023-09-17 21:59:15 +02:00
DebaucheryLibrarian 8b5eada088 1.231.27 2023-08-28 02:53:38 +02:00
DebaucheryLibrarian eeb947d311 Catching media store fail unlink error to prevent batch failure. 2023-08-28 02:53:35 +02:00
DebaucheryLibrarian 430d7a8cdd 1.231.26 2023-08-16 06:31:51 +02:00
DebaucheryLibrarian c94dcdd9e6 Updated Radical to support Top Web Models. 2023-08-16 06:31:49 +02:00
DebaucheryLibrarian 46c514f530 1.231.25 2023-08-16 03:08:50 +02:00
DebaucheryLibrarian b29a34c76a Updated AnalVids. 2023-08-16 03:08:47 +02:00
DebaucheryLibrarian 20ba833147 1.231.24 2023-08-14 01:54:19 +02:00
DebaucheryLibrarian 1ed47c3173 Updated Radical scraper. Added town property to location resolve tool. 2023-08-14 01:54:17 +02:00
DebaucheryLibrarian ccac1f96dd 1.231.23 2023-08-13 21:57:35 +02:00
DebaucheryLibrarian c860bfebc1 Separated error log. Added search/summary update tool. 2023-08-13 21:57:32 +02:00
DebaucheryLibrarian d5806c3d31 1.231.22 2023-08-12 23:19:33 +02:00
DebaucheryLibrarian bf36825fce Added Say Uncle channels. 2023-08-12 23:19:30 +02:00
DebaucheryLibrarian a1780e2c4b 1.231.21 2023-08-11 03:56:12 +02:00
DebaucheryLibrarian 02850eb6e3 Updated Team Skeet and MYLF. 2023-08-11 03:56:08 +02:00
DebaucheryLibrarian 71efd7a96f 1.231.20 2023-08-05 01:39:02 +02:00
DebaucheryLibrarian 3508e47600 Added alt_titles to series table. 2023-08-05 01:38:58 +02:00
DebaucheryLibrarian eef0be09b3 1.231.19 2023-08-05 00:09:15 +02:00
DebaucheryLibrarian 2df20aea38 Moved Aziani to Gamma. 2023-08-05 00:09:13 +02:00
DebaucheryLibrarian 1032c3cb57 1.231.18 2023-08-04 22:30:20 +02:00
DebaucheryLibrarian 5f26f8de27 Excluding studios from entity aggregration. Added profile scraping to Teste de Fudelidade. 2023-08-04 22:30:18 +02:00
DebaucheryLibrarian 328550b9a3 Added Teste de Fudelidade. 2023-08-04 22:02:28 +02:00
DebaucheryLibrarian 28455ecd5d 1.231.17 2023-08-04 21:43:58 +02:00
DebaucheryLibrarian 54dbea659a Added missing image fallbacks to legacy Aziani scraper. 2023-08-04 21:43:57 +02:00
DebaucheryLibrarian 447c1d748a 1.231.16 2023-08-04 00:12:27 +02:00
DebaucheryLibrarian 4a10887e48 Fixed Aziani breaking on missing source. 2023-08-04 00:12:24 +02:00
DebaucheryLibrarian 48015a4c95 1.231.15 2023-08-03 23:35:14 +02:00
DebaucheryLibrarian abbfedf3f7 Added trailer support to legacy Aziani scraper. 2023-08-03 23:35:10 +02:00
DebaucheryLibrarian fa9153deb8 1.231.14 2023-08-03 23:24:26 +02:00
DebaucheryLibrarian b287f5c2db Fixed photo query in Aziana scraper. 2023-08-03 23:24:23 +02:00
DebaucheryLibrarian 2ebc2d441f 1.231.13 2023-08-03 00:31:09 +02:00
DebaucheryLibrarian 5f2c9eb5df Fixed place cache check. 2023-08-03 00:31:07 +02:00
DebaucheryLibrarian ce41e24434 1.231.12 2023-08-02 23:12:44 +02:00
DebaucheryLibrarian c5d81e94e5 Updated OSM API path, added redis caching. 2023-08-02 23:12:41 +02:00
DebaucheryLibrarian 0500bdee2b 1.231.11 2023-08-02 16:38:53 +02:00
DebaucheryLibrarian e9a1df9123 Improved Model Media scene code matching. 2023-08-02 16:38:50 +02:00
DebaucheryLibrarian 277a06c3de 1.231.10 2023-08-02 02:22:31 +02:00
DebaucheryLibrarian c064efc812 Fixed title curation disparity Tokyo Hot. 2023-08-02 02:22:26 +02:00
DebaucheryLibrarian 1408074ef4 1.231.9 2023-08-02 02:14:46 +02:00
DebaucheryLibrarian 8c1f1b69ff Splitting Han titles and actors in Model Media scraper. 2023-08-02 02:14:41 +02:00
DebaucheryLibrarian 5783507344 1.231.8 2023-07-31 23:41:35 +02:00
DebaucheryLibrarian ae64c5225f Added conditions to Tokyo Hot scraper to prevent total failure. 2023-07-31 23:41:32 +02:00
DebaucheryLibrarian d2f81d446b 1.231.7 2023-07-29 23:59:20 +02:00
DebaucheryLibrarian ab5b165c68 Preserving Jules Jordan base photos in deep scrape. 2023-07-29 23:59:17 +02:00
DebaucheryLibrarian c3d4bf0e62 Capitalized JAV tag. 2023-07-26 00:28:03 +02:00
DebaucheryLibrarian 60f594a948 1.231.6 2023-07-26 00:26:17 +02:00
DebaucheryLibrarian 7f74d227f0 Tracking iframe campaign clicks. 2023-07-26 00:26:15 +02:00
DebaucheryLibrarian f59429c30a 1.231.5 2023-07-25 20:45:50 +02:00
DebaucheryLibrarian 40276a11f9 Fixed Tokyo Hot deep scrape failing on missing trailer. 2023-07-25 20:45:46 +02:00
DebaucheryLibrarian 0e846bec3f 1.231.4 2023-07-25 05:17:33 +02:00
DebaucheryLibrarian ba376fa074 Getting last showcased batch for new label. 2023-07-25 05:17:30 +02:00
DebaucheryLibrarian d1b54dc2c5 1.231.3 2023-07-25 05:05:09 +02:00
DebaucheryLibrarian b9c3efa24e Tracking image campaign clicks in umami. 2023-07-25 05:05:05 +02:00
DebaucheryLibrarian 40e613ed8d 1.231.2 2023-07-25 04:23:13 +02:00
DebaucheryLibrarian d033def947 Using batch showcase flag to hide archival releases. 2023-07-25 04:23:10 +02:00
DebaucheryLibrarian 671e110d99 1.231.1 2023-07-25 03:22:26 +02:00
DebaucheryLibrarian b7a31b7933 Added showcase flag to batch table. 2023-07-25 03:22:24 +02:00
DebaucheryLibrarian 1082effc17 1.231.0 2023-07-25 03:03:45 +02:00
DebaucheryLibrarian 693983dc29 Added screen caps separate from photos. Added Tokyo Hot. Added hair type, shoe size and blood type actor fields. 2023-07-25 03:03:41 +02:00
DebaucheryLibrarian 6fe212796b 1.230.1 2023-07-24 22:21:09 +02:00
DebaucheryLibrarian bb1aa4aa55 Added Umami to pm2 ecosystem file. 2023-07-24 22:21:05 +02:00
DebaucheryLibrarian 7d77e0603b 1.230.0 2023-07-24 00:59:49 +02:00
DebaucheryLibrarian 9f727a0fa0 Added analytics configuration. 2023-07-24 00:59:47 +02:00
DebaucheryLibrarian 997914ec27 Improved Hush T1 profile scraper. 2023-07-23 01:05:25 +02:00
DebaucheryLibrarian 37f01b68e8 1.229.6 2023-07-23 01:02:20 +02:00
DebaucheryLibrarian ca695db3ba Storing actor socials, improved Hush profile scraper. 2023-07-23 01:02:18 +02:00
DebaucheryLibrarian 48acabac49 1.229.5 2023-07-22 23:43:49 +02:00
DebaucheryLibrarian d7ee278b02 Removed unnecessary actor details from releases query. 2023-07-22 23:43:44 +02:00
DebaucheryLibrarian 191a3628b5 1.229.4 2023-07-22 00:50:02 +02:00
DebaucheryLibrarian 80750b44dc Added Milfy to profile list, fixed Vixen not returning gracefully when profile is not found. 2023-07-22 00:49:56 +02:00
DebaucheryLibrarian 25f3dcf9a5 Fixed avatar data type in Vixen scraper. 2023-07-22 00:01:12 +02:00
DebaucheryLibrarian c17e44e9f9 1.229.3 2023-07-21 23:55:33 +02:00
DebaucheryLibrarian 0b101dde3c Updated Vixen model scraper. 2023-07-21 23:55:30 +02:00
DebaucheryLibrarian 4d20dae079 1.229.2 2023-07-21 21:58:52 +02:00
DebaucheryLibrarian 86e4fb7603 Fixed Team Skeet profiles. 2023-07-21 21:58:50 +02:00
DebaucheryLibrarian e8d081cc67 1.229.1 2023-07-21 01:07:08 +02:00
DebaucheryLibrarian dab38c8013 Added grey hair color, removed console log. 2023-07-21 01:07:06 +02:00
DebaucheryLibrarian c29ecac41c Fixed summary view refresh. 2023-07-17 02:56:37 +02:00
DebaucheryLibrarian 4187f5e7e8 1.229.0 2023-07-17 01:59:29 +02:00
DebaucheryLibrarian 2b3b2d7fd2 Using summaries table for improved performance. 2023-07-17 01:59:26 +02:00
DebaucheryLibrarian 2783de5272 1.228.33 2023-07-09 21:39:44 +02:00
DebaucheryLibrarian 77727dff77 Added MariskaX. 2023-07-09 21:39:40 +02:00
DebaucheryLibrarian f009c90e5d Updated Blowpass seed entries. 2023-07-09 05:23:17 +02:00
DebaucheryLibrarian f4cb4ca26a 1.228.32 2023-07-09 05:08:37 +02:00
DebaucheryLibrarian 1cba51fbfd Moved Blowpass to Gamma API. 2023-07-09 05:08:35 +02:00
DebaucheryLibrarian c5d9b93263 1.228.31 2023-07-09 04:35:33 +02:00
DebaucheryLibrarian 88a56794aa Separated Filthy Kings into its channels, upgraded Gamma scraper to accomodate. 2023-07-09 04:35:30 +02:00
DebaucheryLibrarian c51577098a Added Milfy to proxy list. 2023-07-07 03:52:10 +02:00
DebaucheryLibrarian 3dff352399 1.228.30 2023-07-07 03:43:22 +02:00
DebaucheryLibrarian 6cb48647a8 Updated deepthroat tag photo. 2023-07-07 03:43:16 +02:00
DebaucheryLibrarian d6c6c3435d 1.228.29 2023-07-07 02:20:09 +02:00
DebaucheryLibrarian 6f4608ba23 Using better video API for Vixen deep scrape. 2023-07-07 02:20:07 +02:00
DebaucheryLibrarian 10ba67fde1 1.228.28 2023-07-07 00:23:04 +02:00
DebaucheryLibrarian 83e22813f3 Using more aggressive optional chaining in Vixen scraper to prevent errors. 2023-07-07 00:23:02 +02:00
DebaucheryLibrarian f8e7ace89f Removed superfluous console logs. 2023-07-06 05:40:23 +02:00
DebaucheryLibrarian 8bb46c5a6d Moved missing slug return in Vixen scraper. 2023-07-06 05:30:51 +02:00
DebaucheryLibrarian 6e79112f3a 1.228.27 2023-07-06 05:09:08 +02:00
DebaucheryLibrarian 51e04e7331 Updated Jules Jordan profile scraper. 2023-07-06 05:09:05 +02:00
DebaucheryLibrarian 9331c0af52 1.228.26 2023-07-06 04:24:51 +02:00
DebaucheryLibrarian 18744372b3 Updated Vixen scraper with more informative API query. 2023-07-06 04:24:47 +02:00
DebaucheryLibrarian 43d8b93953 1.228.25 2023-07-06 00:42:14 +02:00
DebaucheryLibrarian b0c0b1a792 Reserving campaign space on actor, entity and tag pages. 2023-07-06 00:42:12 +02:00
DebaucheryLibrarian 717f07a09a 1.228.24 2023-07-06 00:30:06 +02:00
DebaucheryLibrarian f6c1910be3 Added media attempt configuration, default from 3 to 2, 1 for JJ scraper. 2023-07-06 00:30:04 +02:00
DebaucheryLibrarian a6077599bb 1.228.23 2023-07-06 00:14:42 +02:00
DebaucheryLibrarian 0905847ffa Updated Jules Jordan scraper. 2023-07-06 00:14:38 +02:00
DebaucheryLibrarian 66439b3b17 1.228.22 2023-07-03 00:50:50 +02:00
DebaucheryLibrarian 916deff487 Fixed Arch Angel placeholder thumbnail. 2023-07-03 00:50:47 +02:00
DebaucheryLibrarian 05788c2ed6 1.228.21 2023-07-02 23:59:51 +02:00
DebaucheryLibrarian 13d02a44e5 Fixed failed hash duplicate source breaking media association. 2023-07-02 23:59:49 +02:00
DebaucheryLibrarian a2ff12a636 1.228.20 2023-07-02 22:21:21 +02:00
DebaucheryLibrarian 0a27e91de7 Using effective date in all queries. 2023-07-02 22:21:19 +02:00
DebaucheryLibrarian 3c8b6e6fc1 1.228.19 2023-07-02 21:06:42 +02:00
DebaucheryLibrarian 61c84e18e4 Added separate force media argument. 2023-07-02 21:06:38 +02:00
DebaucheryLibrarian a858b2409a 1.228.18 2023-07-02 05:17:30 +02:00
DebaucheryLibrarian bb204f3d85 Disabled Arch Angel campaigns until NATS license is reactivated. 2023-07-02 05:17:27 +02:00
DebaucheryLibrarian 38ce9c84ba 1.228.17 2023-07-02 05:13:43 +02:00
DebaucheryLibrarian 57a8b8e2f6 Added setting to disable campaigns. 2023-07-02 05:13:40 +02:00
DebaucheryLibrarian 4a3674feac Added dedicated Arch Angel scraper. 2023-07-02 05:07:38 +02:00
DebaucheryLibrarian e22dbb315e 1.228.16 2023-07-02 01:04:41 +02:00
DebaucheryLibrarian a339c096ef Fixed Kink trailer query. 2023-07-02 01:04:39 +02:00
DebaucheryLibrarian a8fa1f36f8 Added Van Styles to Jules Jordan tag directors. 2023-07-01 22:45:28 +02:00
DebaucheryLibrarian 6edd587a33 1.228.15 2023-07-01 22:24:23 +02:00
DebaucheryLibrarian a2331bc913 Added prefer option for entity resolution. Merged migrations. 2023-07-01 22:24:21 +02:00
DebaucheryLibrarian 744bdb3170 1.228.14 2023-07-01 21:53:03 +02:00
DebaucheryLibrarian aa9e3b3d1f Deriving Jules Jordan director from tags. 2023-07-01 21:53:01 +02:00
DebaucheryLibrarian 99f2faa328 1.228.13 2023-07-01 21:46:47 +02:00
DebaucheryLibrarian 205102ff90 Added Meiden van Holland and Vurig Vlaanderen. 2023-07-01 21:46:44 +02:00
DebaucheryLibrarian 01b3cc42af 1.228.12 2023-06-24 22:30:58 +02:00
DebaucheryLibrarian 6de6053eaa Added page title function for Composition API components. 2023-06-24 22:30:55 +02:00
DebaucheryLibrarian fdad61465c 1.228.11 2023-06-24 17:32:55 +02:00
DebaucheryLibrarian 49a08cd576 Using time distance for content updated on stats page. 2023-06-24 17:32:53 +02:00
DebaucheryLibrarian 60c4f6e6c1 1.228.10 2023-06-24 17:23:44 +02:00
DebaucheryLibrarian 57e7710f25 Using locale numbers for stats page. 2023-06-24 17:23:42 +02:00
DebaucheryLibrarian e41d1e1ad2 1.228.9 2023-06-24 17:16:10 +02:00
DebaucheryLibrarian fbcf17d1c4 Refactored stats page. 2023-06-24 17:16:06 +02:00
DebaucheryLibrarian f4ed4fb8d8 1.228.8 2023-06-22 01:31:39 +02:00
DebaucheryLibrarian 25a90dd52c Moved Dogfart to DFXtra. 2023-06-22 01:31:36 +02:00
DebaucheryLibrarian ed92919c0d 1.228.7 2023-06-19 05:19:51 +02:00
DebaucheryLibrarian b5309005e9 Using affiliate parameters for Bang, fixed affiliate URL composition on entity page. 2023-06-19 05:19:49 +02:00
DebaucheryLibrarian 54c501e277 Added Bang affiliate link. 2023-06-19 05:11:27 +02:00
DebaucheryLibrarian f36d0686a1 1.228.6 2023-06-19 03:56:07 +02:00
DebaucheryLibrarian d6b44615a0 Allow HTML in disclaimer and announcement. 2023-06-19 03:56:05 +02:00
DebaucheryLibrarian 687d4aec08 1.228.5 2023-06-19 03:28:57 +02:00
DebaucheryLibrarian 18f75595da Added GraphiQL disable. 2023-06-19 03:28:55 +02:00
DebaucheryLibrarian 123d4155b4 1.228.4 2023-06-19 02:51:05 +02:00
DebaucheryLibrarian b362f95790 Added pm2 ecosystem file. 2023-06-19 02:51:01 +02:00
DebaucheryLibrarian 34613a92c5 1.228.3 2023-06-19 01:45:47 +02:00
DebaucheryLibrarian 1766556c49 Added query timeout setting. 2023-06-19 01:45:45 +02:00
DebaucheryLibrarian 6bf7fc5655 1.228.2 2023-06-19 01:36:06 +02:00
DebaucheryLibrarian 7bfb08f524 Added header notice, 2023-06-19 01:36:04 +02:00
DebaucheryLibrarian c4e77acdee 1.228.1 2023-06-19 01:25:33 +02:00
DebaucheryLibrarian 67c1bc6b1c Selecting parent and grantparent networks in entity releases filter. 2023-06-19 01:25:31 +02:00
DebaucheryLibrarian 4e6b098448 1.228.0 2023-06-19 00:39:03 +02:00
DebaucheryLibrarian dbaddfb291 Merged database migrations. 2023-06-19 00:39:00 +02:00
DebaucheryLibrarian 85942c5d00 1.227.18 2023-06-18 02:29:12 +02:00
DebaucheryLibrarian fe460f7441 Querying entity scenes from top level. 2023-06-18 02:29:10 +02:00
DebaucheryLibrarian 67365507b5 1.227.17 2023-06-18 02:12:14 +02:00
DebaucheryLibrarian 87a29baf8b Added stream ignore option. 2023-06-18 02:12:13 +02:00
DebaucheryLibrarian 2f4ac4e427 Removed default showcasing from entity showcase migration. 2023-06-17 23:33:15 +02:00
DebaucheryLibrarian 0056780dc4 1.227.16 2023-06-17 23:31:12 +02:00
DebaucheryLibrarian 013675d102 Improved showcase query. 2023-06-17 23:31:09 +02:00
DebaucheryLibrarian 236d4a9427 Fixed showcased not inserted in networks seed file. 2023-06-16 03:18:40 +02:00
DebaucheryLibrarian 22512833da Dsiabled showcase for entirey Nebraska Coeds network. 2023-06-16 03:17:03 +02:00
DebaucheryLibrarian dc231527f3 Disabled showcase for Nebraska Coeds. 2023-06-16 03:14:38 +02:00
DebaucheryLibrarian d4b0f2dc67 1.227.15 2023-06-16 02:29:03 +02:00
DebaucheryLibrarian 7723b2b698 Only curating release ID when present. 2023-06-16 02:29:01 +02:00
DebaucheryLibrarian 682f299c8f Added Milfy to Vixen. 2023-06-16 02:12:31 +02:00
DebaucheryLibrarian c43bef544e 1.227.14 2023-06-16 00:47:23 +02:00
DebaucheryLibrarian c4424f30ec Restored 'new' label client-side. 2023-06-16 00:47:19 +02:00
DebaucheryLibrarian 078837f276 1.227.13 2023-06-16 00:20:28 +02:00
DebaucheryLibrarian 6534692b73 Improved search efficiency. 2023-06-16 00:20:24 +02:00
DebaucheryLibrarian 20f82c4006 1.227.12 2023-06-15 19:53:44 +02:00
DebaucheryLibrarian 128f9950ec Prefer HTML over data titles for capitalization in Bang scraper. 2023-06-15 19:53:42 +02:00
DebaucheryLibrarian c2c329e00a 1.227.11 2023-06-15 16:56:30 +02:00
DebaucheryLibrarian 5d3358ed91 Decoding HTML entities in title, description and location. 2023-06-15 16:56:27 +02:00
DebaucheryLibrarian d7f9157424 1.227.10 2023-06-12 01:43:48 +02:00
DebaucheryLibrarian f464563dae Added index on release tags table and temporarily removed 'new' label for performance. 2023-06-12 01:43:43 +02:00
DebaucheryLibrarian 828db2a8c8 1.227.9 2023-06-11 02:38:04 +02:00
DebaucheryLibrarian bca865068a Fixed upcoming date sorting. 2023-06-11 02:38:02 +02:00
DebaucheryLibrarian 35245ca03f 1.227.8 2023-06-11 01:17:39 +02:00
DebaucheryLibrarian bcc183d5b9 Using native stream promises for media stream retrieval. 2023-06-11 01:17:37 +02:00
DebaucheryLibrarian 433498eaed 1.227.7 2023-06-10 02:06:02 +02:00
DebaucheryLibrarian 80334843c9 Improved puppeteer bypass, enabled for Team Skeet. 2023-06-10 02:05:59 +02:00
DebaucheryLibrarian 09a48ed064 1.227.6 2023-06-10 00:46:41 +02:00
DebaucheryLibrarian bae51dd59c Fixed poster query for old New Sensations scenes. 2023-06-10 00:46:38 +02:00
DebaucheryLibrarian 58175dce21 1.227.5 2023-06-09 00:33:42 +02:00
DebaucheryLibrarian c4e4f649f5 Fixed New Sensations classic scraper breaking on missing actors. 2023-06-09 00:33:40 +02:00
DebaucheryLibrarian bcd3c08faa Simplified stash routing. 2023-06-08 04:23:54 +02:00
DebaucheryLibrarian 0e656ea5ca 1.227.4 2023-06-08 04:19:40 +02:00
DebaucheryLibrarian d847c58d24 Changed stash routing. 2023-06-08 04:19:37 +02:00
DebaucheryLibrarian 81f504f33e 1.227.3 2023-06-08 03:57:53 +02:00
DebaucheryLibrarian 914838e367 Curating usernames in sign-up and stash load tool. 2023-06-08 03:57:50 +02:00
DebaucheryLibrarian 1fc441670b Reduced stash load verbosity. 2023-06-08 03:48:29 +02:00
DebaucheryLibrarian a16ca716da Warn instead of error when stash import user does not exist. 2023-06-08 03:42:26 +02:00
DebaucheryLibrarian d0b19752e1 1.227.2 2023-06-08 03:22:48 +02:00
DebaucheryLibrarian 9c63b31dfa Fixed alert tool to transfer combinations. 2023-06-08 03:22:46 +02:00
DebaucheryLibrarian 9bdd3ff2f3 Added alert transfer tools to repo. 2023-06-08 02:37:39 +02:00
DebaucheryLibrarian 4429169166 1.227.1 2023-06-08 02:36:50 +02:00
DebaucheryLibrarian 3dbb74a1dc Added alert transfer tools. Removed stash and alert add-tiles in favor of more prominent heading buttons. 2023-06-08 02:36:47 +02:00
DebaucheryLibrarian e7b72f5e99 Added filename to stash save. 2023-06-08 01:39:44 +02:00
DebaucheryLibrarian 5576fed590 Fixed existing stash selection in transfer tool. 2023-06-08 01:37:27 +02:00
DebaucheryLibrarian aa0fd3cf48 1.227.0 2023-06-08 01:16:48 +02:00
DebaucheryLibrarian 56534800d8 Added stash transfer tool. 2023-06-08 01:16:44 +02:00
DebaucheryLibrarian f7708e0740 1.226.11 2023-06-05 04:49:46 +02:00
DebaucheryLibrarian e36ba59d27 Auto-recognizing m3u8 sources as stream. Fixed Bang Bros poster and trailer. 2023-06-05 04:49:44 +02:00
DebaucheryLibrarian a99cee38a0 1.226.10 2023-06-05 03:32:28 +02:00
DebaucheryLibrarian d3da2359de Refactored Bang! scraper, added My Stepdaughters Friend. 2023-06-05 03:32:24 +02:00
DebaucheryLibrarian adda78f0c6 Refactored New Sensations scraper. 2023-06-05 02:13:36 +02:00
DebaucheryLibrarian 164757ee26 Matching URLs to entity using hostname rather than slug to minimize collisions. Fixed missing Cum Louder POV logo. 2023-06-04 21:50:59 +02:00
DebaucheryLibrarian 7e2840a00d 1.226.9 2023-06-04 04:17:06 +02:00
DebaucheryLibrarian caf37ba9fb Transferring release media types separately to prevent race conditions. 2023-06-04 04:17:01 +02:00
DebaucheryLibrarian 042d3be4a9 1.226.8 2023-06-04 01:24:20 +02:00
DebaucheryLibrarian 18e91d54f1 Transfer tool finds existing media by source. 2023-06-04 01:24:18 +02:00
DebaucheryLibrarian 84c59bd05a Removed redundant description replace in transfer tool. 2023-06-04 01:20:38 +02:00
DebaucheryLibrarian e0f7db8187 Improved skipped scene reporting in transfer tool. 2023-06-04 01:13:23 +02:00
DebaucheryLibrarian 13e38c487f Ignoring skipped scenes in final tally. 2023-06-04 01:05:59 +02:00
DebaucheryLibrarian 5b6911fd5c 1.226.7 2023-06-04 01:04:23 +02:00
DebaucheryLibrarian 33cab26d3b Fixed transfer status, moved media logging to debug level. 2023-06-04 01:04:21 +02:00
DebaucheryLibrarian c9201430ea Logging index in transfer. 2023-06-04 00:58:35 +02:00
DebaucheryLibrarian 48eeac6d88 1.226.6 2023-06-04 00:49:23 +02:00
DebaucheryLibrarian a4c82a377b Returning existing release ID in case new scene for existing movie was encountered. 2023-06-04 00:49:21 +02:00
DebaucheryLibrarian 421e8d0763 1.226.5 2023-06-04 00:42:31 +02:00
DebaucheryLibrarian 66f4244779 Logging skipped releases. 2023-06-04 00:42:29 +02:00
DebaucheryLibrarian 7fb832028e 1.226.4 2023-06-04 00:23:47 +02:00
DebaucheryLibrarian 54798f87da Improved duplicate media handling in transfer tool. 2023-06-04 00:23:45 +02:00
DebaucheryLibrarian 5ad5708e15 1.226.3 2023-06-03 22:41:18 +02:00
DebaucheryLibrarian f356135722 Using line-separated JSON to save memory in transfer tool. 2023-06-03 22:41:15 +02:00
DebaucheryLibrarian f3abc21482 1.226.2 2023-06-03 21:51:12 +02:00
DebaucheryLibrarian 5103a07e5f Accumulating boolean instead of full scene. 2023-06-03 21:51:09 +02:00
DebaucheryLibrarian 3fc63b1934 1.226.1 2023-06-03 02:51:45 +02:00
DebaucheryLibrarian 4b9a0e6bab Extended and improved transfer tool. Moved scenes up on movie page. 2023-06-03 02:51:42 +02:00
DebaucheryLibrarian 62617ec6bf 1.226.0 2023-05-31 00:30:00 +02:00
DebaucheryLibrarian 1b4d973e7b Added transfer tool, WIP. Added Savage Gangbang to Kink. 2023-05-31 00:29:54 +02:00
2397 changed files with 42492 additions and 3046 deletions

3
.gitignore vendored
View File

@ -10,6 +10,9 @@ config/*
!config/default.js !config/default.js
assets/js/config/ assets/js/config/
!assets/js/config/default.js !assets/js/config/default.js
/export*
/stashes*
/alerts*
*.heapprofile *.heapprofile
*.heapsnapshot *.heapsnapshot
.vscode .vscode

View File

@ -65,6 +65,7 @@
</a> </a>
<ul class="bio nolist"> <ul class="bio nolist">
<!-- probably not a good idea
<li <li
v-if="actor.realName" v-if="actor.realName"
class="bio-item" class="bio-item"
@ -72,6 +73,7 @@
<dfn class="bio-label"><Icon icon="vcard" />Real name</dfn> <dfn class="bio-label"><Icon icon="vcard" />Real name</dfn>
<span class="bio-value">{{ actor.realName }}</span> <span class="bio-value">{{ actor.realName }}</span>
</li> </li>
-->
<li <li
v-if="actor.dateOfBirth" v-if="actor.dateOfBirth"
@ -109,6 +111,15 @@
>{{ actor.ageAtDeath }}</span></span> >{{ actor.ageAtDeath }}</span></span>
</li> </li>
<li
v-if="actor.orientation"
class="bio-item"
>
<dfn class="bio-label"><Icon icon="heart7" />Orientation</dfn>
<span class="orientation">{{ actor.orientation }}</span>
</li>
<li <li
v-if="actor.origin" v-if="actor.origin"
class="bio-item birth" class="bio-item birth"
@ -294,7 +305,7 @@
class="description" class="description"
> >
{{ description.text }} {{ description.text }}
<router-link :to="`/${description.entity.type}/${description.entity.slug}`"> <RouterLink :to="`/${description.entity.type}/${description.entity.slug}`">
<img <img
v-if="description.entity.type === 'network' || !description.entity.parent || description.entity.independent" v-if="description.entity.type === 'network' || !description.entity.parent || description.entity.independent"
:src="`/img/logos/${description.entity.slug}/thumbs/network.png`" :src="`/img/logos/${description.entity.slug}/thumbs/network.png`"
@ -306,7 +317,7 @@
:src="`/img/logos/${description.entity.parent.slug}/thumbs/${description.entity.slug}.png`" :src="`/img/logos/${description.entity.parent.slug}/thumbs/${description.entity.slug}.png`"
class="description-logo" class="description-logo"
> >
</router-link> </RouterLink>
</p> </p>
</div> </div>
</div> </div>
@ -380,6 +391,8 @@
</template> </template>
<script> <script>
import config from 'config';
import Pagination from '../pagination/pagination.vue'; import Pagination from '../pagination/pagination.vue';
import FilterBar from '../filters/filter-bar.vue'; import FilterBar from '../filters/filter-bar.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
@ -471,7 +484,7 @@ export default {
releases: null, releases: null,
done: false, done: false,
totalCount: 0, totalCount: 0,
limit: 20, limit: Number(this.$route.query.limit || 30) - config.campaigns.tiles, // reserve one campaign spot
pageTitle: null, pageTitle: null,
bioExpanded: false, bioExpanded: false,
photosExpanded: false, photosExpanded: false,
@ -685,7 +698,8 @@ export default {
.ethnicity, .ethnicity,
.hair, .hair,
.eyes { .eyes,
.orientation {
text-transform: capitalize; text-transform: capitalize;
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<router-link <RouterLink
:to="`/actor/${actor.id}/${actor.slug}`" :to="`/actor/${actor.id}/${actor.slug}`"
:target="target"
class="actor nolink" class="actor nolink"
> >
<div class="avatar"> <div class="avatar">
@ -18,7 +19,7 @@
</div> </div>
<span class="name">{{ actor.name }}</span> <span class="name">{{ actor.name }}</span>
</router-link> </RouterLink>
</template> </template>
<script> <script>
@ -36,6 +37,10 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
target: {
type: String,
default: null,
},
}, },
methods: { methods: {
unstashActor, unstashActor,

View File

@ -41,9 +41,9 @@
/> />
</span> </span>
<a <RouterLink
:to="hasScrolled ? '' : { name: 'actor', params: { actorId: actor.id, actorSlug: actor.slug } }"
class="avatar-container" class="avatar-container"
@click="goToActor"
> >
<img <img
v-if="actor.avatar" v-if="actor.avatar"
@ -67,21 +67,21 @@
v-show="(!stash || stash.primary) && favorited" v-show="(!stash || stash.primary) && favorited"
icon="heart7" icon="heart7"
class="stash stashed" class="stash stashed"
@click.prevent.native="unstashActor" @click.stop.native="unstashActor"
/> />
<Icon <Icon
v-show="(!stash || stash.primary) && favorited === false" v-show="(!stash || stash.primary) && favorited === false"
icon="heart8" icon="heart8"
class="stash unstashed" class="stash unstashed"
@click.prevent.native="stashActor" @click.stop.native="stashActor"
/> />
<Icon <Icon
v-show="stash && !stash.primary" v-show="stash && !stash.primary"
icon="cross2" icon="cross2"
class="stash unstash" class="stash unstash"
@click.prevent.native="unstashActor" @click.stop.native="unstashActor"
/> />
<span class="details"> <span class="details">
@ -130,7 +130,7 @@
class="country" class="country"
/> />
</span> </span>
</a> </RouterLink>
</div> </div>
</div> </div>
</template> </template>
@ -138,13 +138,6 @@
<script> <script>
import Gender from './gender.vue'; import Gender from './gender.vue';
function goToActor() {
// can't seem to control behavior with RouterLink
if (!this.hasScrolled) {
this.$router.push({ name: 'actor', params: { actorId: this.actor.id, actorSlug: this.actor.slug } });
}
}
async function stashActor() { async function stashActor() {
this.favorited = true; this.favorited = true;
@ -215,7 +208,6 @@ export default {
methods: { methods: {
stashActor, stashActor,
unstashActor, unstashActor,
goToActor,
}, },
}; };
</script> </script>

View File

@ -3,13 +3,28 @@
title="Add alert" title="Add alert"
@close="$emit('close')" @close="$emit('close')"
> >
<div
v-if="error"
class="dialog-error"
>{{ error }}</div>
<form <form
class="dialog-body" class="dialog-body"
@submit.prevent="addAlert" @submit.prevent="addAlert"
> >
<div class="dialog-section"> <div class="dialog-section">
<h3 class="dialog-heading"> <h3 class="dialog-heading">
When<span class="dialog-description">All to appear in the same scene</span> When
<label class="dialog-description noselect">
<template v-if="all">Scene must match&nbsp;<strong>all</strong>&nbsp;fields</template>
<template v-else>Scene must match&nbsp;<strong>any</strong>&nbsp;field</template>
<Toggle
:checked="all"
@change="(checked) => all = checked"
/>
</label>
</h3> </h3>
<div class="alert-section"> <div class="alert-section">
@ -29,7 +44,10 @@
:key="`actor-${actor.id}`" :key="`actor-${actor.id}`"
class="actor" class="actor"
> >
<ActorPreview :actor="actor" /> <ActorPreview
:actor="actor"
target="_blank"
/>
<Icon <Icon
icon="cross3" icon="cross3"
@ -104,10 +122,15 @@
<div class="entities"> <div class="entities">
<div <div
v-if="entity" v-for="(entity, index) in entities"
:key="`entity-${entity.id}`"
:class="{ invalid: all && index > 0 }"
class="entity" class="entity"
> >
<Entity :entity="entity" /> <Entity
:entity="entity"
target="_blank"
/>
<Icon <Icon
icon="cross3" icon="cross3"
@ -116,7 +139,7 @@
/> />
</div> </div>
<Tooltip v-if="!entity"> <Tooltip v-if="entities.length < 1 || !all">
<div class="entity placeholder"> <div class="entity placeholder">
Any channel Any channel
@ -136,6 +159,72 @@
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div class="alert-section">
<h4 class="alert-heading">Matching</h4>
<ul class="matches nolist">
<li
v-for="(match, index) in matches"
:key="`match-${index}`"
class="match"
>
<span class="match-property">{{ match.property }}:&nbsp;</span>
<span
v-if="match.expression.slice(0, 1) === '/' && match.expression.slice(-1) === '/'"
class="match-expression"
><span class="match-slash">/</span>{{ match.expression.slice(1, -1) }}<span class="match-slash">/</span></span>
<span
v-else
class="match-expression"
>{{ match.expression }}</span>
<Icon
icon="cross3"
class="remove"
@click.native="removeMatch(index)"
/>
</li>
<Tooltip
v-if="!entity"
@open="$refs.expression?.focus()"
>
<li class="match placeholder">
Anything
<Icon
icon="plus3"
class="add"
/>
</li>
<template #tooltip>
<form
class="pattern-tooltip"
@submit.prevent="addMatch"
>
<select
v-model="matchProperty"
class="input"
>
<option value="title">Title</option>
<option value="description">Description</option>
</select>
<input
ref="expression"
v-model="matchExpression"
class="input"
placeholder="Expression, // for RegExp"
>
</form>
</template>
</Tooltip>
</ul>
</div>
</div> </div>
<div class="dialog-section"> <div class="dialog-section">
@ -194,7 +283,7 @@
<div class="dialog-actions right"> <div class="dialog-actions right">
<button <button
:disabled="actors.length === 0 && tags.length === 0 && !entity" :disabled="actors.length === 0 && tags.length === 0 && !entity && matches.length === 0"
type="submit" type="submit"
class="button button-primary" class="button button-primary"
>Add alert</button> >Add alert</button>
@ -207,19 +296,28 @@
import ActorPreview from '../actors/preview.vue'; import ActorPreview from '../actors/preview.vue';
import Entity from '../entities/tile.vue'; import Entity from '../entities/tile.vue';
import Checkbox from '../form/checkbox.vue'; import Checkbox from '../form/checkbox.vue';
import Toggle from '../form/toggle.vue';
import Search from './search.vue'; import Search from './search.vue';
async function addAlert() { async function addAlert() {
this.error = null;
try {
await this.$store.dispatch('addAlert', { await this.$store.dispatch('addAlert', {
all: this.all,
actors: this.actors.map((actor) => actor.id), actors: this.actors.map((actor) => actor.id),
tags: this.tags.map((tag) => tag.id), tags: this.tags.map((tag) => tag.id),
entity: this.entity?.id, matches: this.matches,
entities: this.entities.map((entity) => entity.id),
notify: this.notify, notify: this.notify,
email: this.email, email: this.email,
stashes: this.stashes.map((stash) => stash.id), stashes: this.stashes.map((stash) => stash.id),
}); });
this.$emit('close', true); this.$emit('close', true);
} catch (error) {
this.error = error.message;
}
} }
function addActor(actor) { function addActor(actor) {
@ -231,7 +329,7 @@ function addActor(actor) {
} }
function addEntity(entity) { function addEntity(entity) {
this.entity = entity; this.entities = this.entities.concat(entity);
this.events.emit('blur'); this.events.emit('blur');
} }
@ -247,14 +345,34 @@ function removeActor(actor) {
this.actors = this.actors.filter((listedActor) => listedActor.id !== actor.id); this.actors = this.actors.filter((listedActor) => listedActor.id !== actor.id);
} }
function removeEntity() { function removeEntity(entity) {
this.entity = null; this.entities = this.entities.filter((alertEntity) => alertEntity.id !== entity.id);
} }
function removeTag(tag) { function removeTag(tag) {
this.tags = this.tags.filter((listedTag) => listedTag.id !== tag.id); this.tags = this.tags.filter((listedTag) => listedTag.id !== tag.id);
} }
function addMatch() {
if (!this.matchExpression) {
return;
}
this.matches = this.matches.concat({
property: this.matchProperty,
expression: this.matchExpression,
});
this.matchProperty = 'title';
this.matchExpression = null;
this.events.emit('blur');
}
function removeMatch(removeIndex) {
this.matches = this.matches.filter((match, matchIndex) => matchIndex !== removeIndex);
}
function addStash(stash) { function addStash(stash) {
if (!this.stashes.some((selectedStash) => selectedStash.id === stash.id)) { if (!this.stashes.some((selectedStash) => selectedStash.id === stash.id)) {
this.stashes = this.stashes.concat(stash); this.stashes = this.stashes.concat(stash);
@ -273,13 +391,19 @@ export default {
Checkbox, Checkbox,
Entity, Entity,
Search, Search,
Toggle,
}, },
emits: ['close'], emits: ['close'],
data() { data() {
return { return {
error: null,
actors: [], actors: [],
tags: [], tags: [],
entity: null, all: true,
entities: [],
matches: [],
matchProperty: 'title',
matchExpression: null,
notify: true, notify: true,
email: false, email: false,
stashes: [], stashes: [],
@ -290,10 +414,12 @@ export default {
addActor, addActor,
addAlert, addAlert,
addEntity, addEntity,
addMatch,
addTag, addTag,
addStash, addStash,
removeActor, removeActor,
removeEntity, removeEntity,
removeMatch,
removeTag, removeTag,
removeStash, removeStash,
}, },
@ -320,9 +446,24 @@ export default {
} }
.dialog-description { .dialog-description {
display: flex;
align-items: center;
color: var(--shadow); color: var(--shadow);
font-size: .9rem; font-size: .9rem;
font-weight: normal; font-weight: normal;
.toggle-container {
margin-left: .5rem;
}
}
.dialog-error {
padding: 1rem;
margin-bottom: 1rem;
background: var(--error);
color: var(--text-light);
font-weight: bold;
text-align: center;
} }
.alert-heading { .alert-heading {
@ -338,6 +479,34 @@ export default {
font-size: 0; font-size: 0;
} }
.match {
display: flex;
align-items: center;
padding: .25rem 0;
font-family: inherit;
.remove {
position: relative;
top: -.1rem;
right: 0;
}
}
.match-property {
text-transform: capitalize;
color: var(--shadow);
}
.match-expression {
flex-grow: 1;
}
.match-slash {
padding: 0 .1rem;
color: var(--primary);
font-weight: bold;
}
.actors > .actor, .actors > .actor,
.entity, .entity,
.tag, .tag,
@ -347,6 +516,11 @@ export default {
margin: 0 .5rem .5rem 0; margin: 0 .5rem .5rem 0;
} }
.entity.invalid {
opacity: .5;
pointer-events: none;
}
.entity .tile { .entity .tile {
width: 10rem; width: 10rem;
height: 2.5rem; height: 2.5rem;
@ -366,6 +540,14 @@ export default {
color: var(--text); color: var(--text);
} }
.pattern-tooltip {
display: flex;
gap: .5rem;
position: relative;
padding: .5rem;
overflow: hidden;
}
.remove { .remove {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;

View File

@ -1,21 +1,28 @@
<template> <template>
<iframe
v-if="campaign?.banner?.type === 'html'"
ref="iframe"
:width="campaign.banner.width"
:height="campaign.banner.height"
:src="getSource(campaign)"
scrolling="none"
marginwidth="0"
marginheight="0"
class="campaign frame"
data-umami-event="campaign-click"
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
/>
<a <a
v-if="campaign" v-else-if="campaign"
:href="campaign.url || campaign.affiliate?.url" :href="campaign.url || campaign.affiliate?.url"
target="_blank" target="_blank"
class="campaign" class="campaign"
data-umami-event="campaign-click"
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
> >
<img <img
v-if="campaign.banner.entity.type === 'network' || !campaign.banner.entity.parent" :src="getSource(campaign)"
:src="`/img/banners/${campaign.banner.entity.slug}/${campaign.banner.id}.${campaign.banner.type || 'jpg'}`"
:width="campaign.banner.width"
:height="campaign.banner.height"
class="campaign-banner"
>
<img
v-if="campaign.banner.entity.type === 'channel' && campaign.banner.entity.parent?.type === 'network'"
:src="`/img/banners/${campaign.banner.entity.parent.slug}/${campaign.banner.entity.slug}/${campaign.banner.id}.${campaign.banner.type || 'jpg'}`"
:width="campaign.banner.width" :width="campaign.banner.width"
:height="campaign.banner.height" :height="campaign.banner.height"
class="campaign-banner" class="campaign-banner"
@ -37,6 +44,11 @@ function ratioFilter(banner) {
return false; return false;
} }
if (banner.type === 'html' && banner.width > window.innerWidth) {
// usually non-scalable iframes
return false;
}
if (this.minRatio && banner.ratio < this.minRatio) { if (this.minRatio && banner.ratio < this.minRatio) {
return false; return false;
} }
@ -48,6 +60,18 @@ function ratioFilter(banner) {
return true; return true;
} }
function getSource(campaign) {
if (campaign.banner.entity.type === 'network' || !campaign.banner.entity.parent) {
return `/banners/${campaign.banner.entity.slug}/${campaign.banner.id}.${campaign.banner.type || 'jpg'}`;
}
if (campaign.banner.entity.type === 'channel' && campaign.banner.entity.parent?.type === 'network') {
return `/banners/${campaign.banner.entity.parent.slug}/${campaign.banner.entity.slug}/${campaign.banner.id}.${campaign.banner.type || 'jpg'}`;
}
return null;
}
function entityCampaign() { function entityCampaign() {
const bannerCampaigns = this.entity.campaigns const bannerCampaigns = this.entity.campaigns
.concat(this.entity.children?.flatMap((child) => child.campaigns)) .concat(this.entity.children?.flatMap((child) => child.campaigns))
@ -63,6 +87,10 @@ function entityCampaign() {
return randomCampaign; return randomCampaign;
} }
if (this.allowGeneric) {
return this.genericCampaign();
}
this.$emit('campaign', null); this.$emit('campaign', null);
return null; return null;
@ -84,6 +112,10 @@ function tagCampaign() {
return randomCampaign; return randomCampaign;
} }
if (this.allowGeneric) {
return this.genericCampaign();
}
this.$emit('campaign', null); this.$emit('campaign', null);
return null; return null;
@ -98,7 +130,21 @@ async function genericCampaign() {
return randomCampaign; return randomCampaign;
} }
async function specificCampaign(campaignId) {
const campaign = await this.$store.dispatch('fetchCampaign', campaignId);
this.campaign = campaign;
this.$emit('campaign', campaign);
return campaign;
}
async function mounted() { async function mounted() {
if (this.$route.query.campaign) {
await this.specificCampaign(this.$route.query.campaign);
return;
}
if (this.entity) { if (this.entity) {
await this.entityCampaign(); await this.entityCampaign();
return; return;
@ -134,6 +180,10 @@ export default {
type: Number, type: Number,
default: null, default: null,
}, },
allowGeneric: {
type: Boolean,
default: false,
},
maxRatio: { maxRatio: {
type: Number, type: Number,
default: null, default: null,
@ -149,7 +199,9 @@ export default {
methods: { methods: {
entityCampaign, entityCampaign,
genericCampaign, genericCampaign,
getSource,
ratioFilter, ratioFilter,
specificCampaign,
tagCampaign, tagCampaign,
}, },
}; };
@ -157,15 +209,29 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.campaign { .campaign {
height: 100%;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: none;
} }
.campaign-banner { .campaign-banner {
height: auto; height: auto;
width: auto;
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
} }
.frame-container {
position: relative;
font-size: 0;
}
.frame-target {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
</style> </style>

View File

@ -16,7 +16,14 @@
<p <p
v-if="config.showDisclaimer" v-if="config.showDisclaimer"
class="disclaimer" class="disclaimer"
>{{ config.disclaimer }}</p> v-html="config.disclaimer"
/>
<p
v-if="config.showAnnouncement"
class="announcement"
v-html="config.announcement"
/>
<div <div
ref="content" ref="content"
@ -89,17 +96,29 @@ function scrollToTop() {
this.$refs.content.scrollTop = 0; this.$refs.content.scrollTop = 0;
} }
function trackIframeCampaign() {
// no way to capture clicks from an iframe directly
if (window.umami && document.activeElement.tagName === 'IFRAME' && document.activeElement.dataset.umamiEvent === 'campaign-click') {
window.umami.track('campaign-click', {
'campaign-id': document.activeElement.dataset.umamiEventCampaignId,
});
}
}
function mounted() { function mounted() {
document.addEventListener('click', this.blur); document.addEventListener('click', this.blur);
window.addEventListener('resize', this.resize); window.addEventListener('resize', this.resize);
this.events.on('toggleSettings', this.toggleSettings); this.events.on('toggleSettings', this.toggleSettings);
this.events.on('toggleSidebar', this.toggleSidebar); this.events.on('toggleSidebar', this.toggleSidebar);
window.addEventListener('blur', this.trackIframeCampaign);
} }
function beforeUnmount() { function beforeUnmount() {
document.removeEventListener('click', this.blur); document.removeEventListener('click', this.blur);
window.removeEventListener('resize', this.resize); window.removeEventListener('resize', this.resize);
window.removeEventListener('blur', this.trackIframeCampaign);
} }
export default { export default {
@ -122,10 +141,11 @@ export default {
mounted, mounted,
beforeUnmount, beforeUnmount,
methods: { methods: {
setConsent,
toggleSidebar, toggleSidebar,
toggleFilters, toggleFilters,
toggleSettings, toggleSettings,
setConsent, trackIframeCampaign,
blur, blur,
resize, resize,
scroll, scroll,
@ -194,13 +214,21 @@ export default {
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.disclaimer { .disclaimer,
.announcement {
padding: .5rem 1rem; padding: .5rem 1rem;
margin: 0; margin: 0;
color: var(--text-light); color: var(--text-light);
background: var(--warn);
font-weight: bold; font-weight: bold;
box-shadow: inset 0 0 3px var(--darken-weak); box-shadow: inset 0 0 3px var(--darken-weak);
text-align: center; text-align: center;
} }
.disclaimer {
background: var(--warn);
}
.announcement {
background: var(--notice);
}
</style> </style>

View File

@ -96,7 +96,10 @@
/> />
</Scroll> </Scroll>
<div class="campaign-container"> <div
v-if="config.campaigns.entity"
class="campaign-container"
>
<Campaign <Campaign
:entity="entity" :entity="entity"
:min-ratio="3" :min-ratio="3"
@ -114,6 +117,7 @@
<div class="releases"> <div class="releases">
<Releases <Releases
:releases="entity.releases" :releases="entity.releases"
:entity="entity"
:done="done" :done="done"
/> />
@ -130,6 +134,8 @@
</template> </template>
<script> <script>
import config from 'config';
import FilterBar from '../filters/filter-bar.vue'; import FilterBar from '../filters/filter-bar.vue';
import Pagination from '../pagination/pagination.vue'; import Pagination from '../pagination/pagination.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
@ -157,12 +163,17 @@ async function fetchEntity(scroll = true) {
const campaign = entity.campaigns.find((campaignX) => !campaignX.banner) const campaign = entity.campaigns.find((campaignX) => !campaignX.banner)
|| entity.parent?.campaigns.find((campaignX) => !campaignX.banner); || entity.parent?.campaigns.find((campaignX) => !campaignX.banner);
if (entity.url) {
const { searchParams, pathname, origin } = new URL(entity.url);
const affiliateParams = new URLSearchParams({ const affiliateParams = new URLSearchParams({
...(entity.url && Object.fromEntries(new URL(entity.url).searchParams)), // preserve any query in entity URL, e.g. ?siteId=5 ...(entity.url && Object.fromEntries(searchParams)), // preserve any query in entity URL, e.g. ?siteId=5
...(campaign?.affiliate?.parameters && Object.fromEntries(new URLSearchParams(campaign.affiliate.parameters))), // append affiliate parameters ...(campaign?.affiliate?.parameters && Object.fromEntries(new URLSearchParams(campaign.affiliate.parameters))), // append affiliate parameters
}).toString(); }).toString();
this.entityUrl = campaign?.url || campaign?.affiliate?.url || `${entity.url}${campaign?.affiliate?.parameters ? `?${affiliateParams}` : ''}`; this.entityUrl = campaign?.url || campaign?.affiliate?.url || `${origin}${pathname}${campaign?.affiliate?.parameters ? `?${affiliateParams}` : ''}`;
}
this.done = true; this.done = true;
if (scroll && this.$refs.filter?.$el) { if (scroll && this.$refs.filter?.$el) {
@ -196,7 +207,7 @@ export default {
pageTitle: null, pageTitle: null,
totalCount: null, totalCount: null,
done: false, done: false,
limit: Number(this.$route.query.limit) || 20, limit: Number(this.$route.query.limit || 30) - config.campaigns.tiles, // reserve one campaign spot
expanded: false, expanded: false,
entityUrl: null, entityUrl: null,
}; };
@ -286,6 +297,7 @@ export default {
} }
.campaign-container { .campaign-container {
max-height: 150px;
background: var(--background-dim); background: var(--background-dim);
text-align: center; text-align: center;
padding: .5rem; padding: .5rem;

View File

@ -1,7 +1,8 @@
<template> <template>
<router-link <RouterLink
:to="`/${entity.type}/${entity.slug}`" :to="`/${entity.type}/${entity.slug}`"
:title="entity.name" :title="entity.name"
:target="target"
class="tile" class="tile"
> >
<div class="tile-logo"> <div class="tile-logo">
@ -47,7 +48,7 @@
<span v-if="typeof entity.sceneTotal !== 'undefined'">{{ entity.sceneTotal }} scenes</span> <span v-if="typeof entity.sceneTotal !== 'undefined'">{{ entity.sceneTotal }} scenes</span>
<span v-if="entity.type === 'network'">{{ entity.childrenTotal }} channels</span> <span v-if="entity.type === 'network'">{{ entity.childrenTotal }} channels</span>
</span> </span>
</router-link> </RouterLink>
</template> </template>
<script> <script>
@ -57,6 +58,10 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
target: {
type: String,
default: null,
},
}, },
emits: ['load'], emits: ['load'],
}; };

View File

@ -9,7 +9,7 @@
<RouterLink <RouterLink
v-if="me && favorites" v-if="me && favorites"
:to="{ name: 'stash', params: { stashId: favorites.id, range: 'scenes', pageNumber: 1 } }" :to="{ name: 'stash', params: { stashId: favorites.id, stashSlug: favorites.slug, username: me.username, range: 'scenes', pageNumber: 1 } }"
class="menu-item" class="menu-item"
><Icon icon="heart7" />Favorites</RouterLink> ><Icon icon="heart7" />Favorites</RouterLink>

View File

@ -99,7 +99,7 @@
<Icon <Icon
v-if="notification.alert" v-if="notification.alert"
v-tooltip="`You set an alert for <strong>${notification.alert.tags.map(tag => tag.name).join(', ') || 'all'}</strong> scenes with <strong>${notification.alert.actors.map(actor => actor.name).join(', ') || 'any actor'}</strong> for <strong>${notification.alert.entity?.name || 'any channel'}</strong>`" v-tooltip="`You set an alert for scenes with <strong>${notification.alert.all ? 'all of' : 'any of'}</strong> <strong>${notification.alert.actors.map(actor => actor.name).join(', ') || 'any actor'}</strong> containing <strong>${notification.alert.tags.map(tag => tag.name).join(', ') || 'any tags'}</strong> from <strong>${notification.alert.entities.map((entity) => entity.name).join(', ') || 'any channel'}</strong> matching <strong>${notification.alert.matches.map((match) => `${match.property}: ${match.expression}`).join(', ') || 'any text'}</strong>`"
icon="question5" icon="question5"
@click.prevent.stop @click.prevent.stop
/> />
@ -145,12 +145,12 @@ export default {
default: 0, default: 0,
}, },
}, },
emits: ['addAlert'],
data() { data() {
return { return {
showAddAlert: false, showAddAlert: false,
}; };
}, },
emits: ['addAlert'],
methods: { methods: {
checkNotifications, checkNotifications,
checkNotification, checkNotification,

View File

@ -1,7 +1,10 @@
<template> <template>
<div class="home"> <div class="home">
<div class="content-inner"> <div class="content-inner">
<div class="campaign-container"> <div
v-if="config.campaigns.home"
class="campaign-container"
>
<Campaign <Campaign
:min-ratio="6" :min-ratio="6"
/> />
@ -35,6 +38,8 @@
</template> </template>
<script> <script>
import config from 'config';
import FilterBar from '../filters/filter-bar.vue'; import FilterBar from '../filters/filter-bar.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
import Pagination from '../pagination/pagination.vue'; import Pagination from '../pagination/pagination.vue';
@ -76,7 +81,7 @@ export default {
releases: [], releases: [],
networks: [], networks: [],
pageTitle: null, pageTitle: null,
limit: 30, limit: Number(this.$route.query.limit || 30) - config.campaigns.tiles, // reserve one campaign spot
totalCount: 0, totalCount: 0,
from: null, from: null,
done: false, done: false,

View File

@ -1,6 +1,7 @@
<template> <template>
<div <div
:class="{ active }" :class="{ active }"
:title="title"
class="icon" class="icon"
v-html="svg" v-html="svg"
/> />

View File

@ -2,7 +2,7 @@
<div class="media-container"> <div class="media-container">
<div <div
class="media" class="media"
:class="{ center: (release.photos?.length || 0) + (release.scenesPhotos?.length || 0) < 2, preview: !me }" :class="{ center: (release.photos?.length || 0) + (release.caps?.length || 0) + (release.scenesPhotos?.length || 0) < 2, preview: !me }"
> >
<div <div
v-if="release.trailer || release.teaser" v-if="release.trailer || release.teaser"
@ -169,9 +169,10 @@ function photos() {
const clips = this.release.clips || []; const clips = this.release.clips || [];
const clipPostersById = clips.reduce((acc, clip) => ({ ...acc, [clip.poster.id]: clip.poster }), {}); const clipPostersById = clips.reduce((acc, clip) => ({ ...acc, [clip.poster.id]: clip.poster }), {});
const uniqueClipPosters = Array.from(new Set(clips.map((clip) => clip.poster.id) || [])).map((posterId) => clipPostersById[posterId]); const uniqueClipPosters = Array.from(new Set(clips.map((clip) => clip.poster.id) || [])).map((posterId) => clipPostersById[posterId]);
const photosWithClipPosters = (this.release.photos || []).concat(this.release.scenesPhotos || []).concat(uniqueClipPosters); const photosWithClipPosters = (this.release.photos || []).concat(this.release.caps || []).concat(this.release.scenesPhotos || []).concat(uniqueClipPosters);
if (this.release.trailer || (this.release.teaser && this.release.teaser.mime !== 'image/gif')) { if (this.release.trailer || (this.release.teaser && this.release.teaser.mime !== 'image/gif')) {
// if (this.release.trailer) {
// poster will be on trailer video // poster will be on trailer video
return photosWithClipPosters; return photosWithClipPosters;
} }

View File

@ -21,21 +21,24 @@
<Details :release="release" /> <Details :release="release" />
<button <button
v-if="release.photos?.length > 0 || release.scenesPhotos?.length > 0" v-if="showAlbum"
class="album-toggle" class="album-toggle"
@click="$router.push({ hash: '#album' })" @click="$router.push({ hash: '#album' })"
><Icon icon="grid3" />View album</button> ><Icon icon="grid3" />View album</button>
<Album <Album
v-if="showAlbum" v-if="showAlbum && $route.hash === '#album'"
:items="[release.poster, ...(release.photos || []), ...(release.scenesPhotos || [])]" :items="[release.poster, ...(release.photos || []), ...(release.caps || []), ...(release.scenesPhotos || [])]"
:title="release.title" :title="release.title"
:path="config.media.mediaPath" :path="config.media.mediaPath"
@close="$router.replace({ hash: undefined })" @close="$router.replace({ hash: undefined })"
/> />
<div class="info column"> <div class="info column">
<div class="row row-title"> <div
class="row row-title"
:class="{ 'has-alt': release.altTitles?.length > 0 }"
>
<h2 <h2
v-if="release.title" v-if="release.title"
class="title" class="title"
@ -62,6 +65,24 @@
/> />
</div> </div>
<div
v-if="release.altTitles?.length > 0"
class="row alttitles"
>
<h2
v-for="(altTitle, index) in release.altTitles"
:key="`altitle-${index}`"
class="alttitle"
>
{{ altTitle }}
</h2>
</div>
<Releases
v-if="release.scenes && release.scenes.length > 0"
:releases="release.scenes"
/>
<div class="row associations"> <div class="row associations">
<ul <ul
ref="actors" ref="actors"
@ -109,11 +130,6 @@
</div> </div>
</div> </div>
<Releases
v-if="release.scenes && release.scenes.length > 0"
:releases="release.scenes"
/>
<div <div
v-if="release.directors && release.directors.length > 0" v-if="release.directors && release.directors.length > 0"
class="row" class="row"
@ -155,6 +171,14 @@
<div class="duration">{{ formatDuration(release.duration) }}</div> <div class="duration">{{ formatDuration(release.duration) }}</div>
</div> </div>
<div
v-if="release.photoCount"
class="row-tidbit"
>
<span class="row-label">Photos</span>
{{ release.photoCount }}
</div>
<div <div
v-if="release.shootId" v-if="release.shootId"
class="row-tidbit" class="row-tidbit"
@ -167,7 +191,13 @@
v-if="release.studio" v-if="release.studio"
class="row-tidbit" class="row-tidbit"
> >
<span class="row-label">Studio</span> <span class="row-label">Studio
<Icon
v-if="release.studio.showcased === false"
icon="eye-blocked"
title="This studio does not appear on main pages"
/>
</span>
<RouterLink <RouterLink
:to="`/studio/${release.studio.slug}`" :to="`/studio/${release.studio.slug}`"
@ -385,7 +415,7 @@ function pageTitle() {
} }
function showAlbum() { function showAlbum() {
return (this.release.photos?.length > 0 || this.release.scenesPhotos?.length > 0) && this.$route.hash === '#album'; return this.release.photos?.length > 0 || this.release.caps?.length > 0 || this.release.scenesPhotos?.length > 0;
} }
async function mounted() { async function mounted() {
@ -471,13 +501,14 @@ export default {
} }
.row-label { .row-label {
display: block; display: flex;
align-items: center;
margin: 0 0 .5rem 0; margin: 0 0 .5rem 0;
color: var(--shadow); color: var(--shadow);
font-weight: bold; font-weight: bold;
.icon { .icon {
margin: 0 .5rem 0 0; margin: 0 .5rem;
fill: var(--shadow); fill: var(--shadow);
} }
} }
@ -513,6 +544,11 @@ export default {
color: var(--shadow); color: var(--shadow);
} }
.alttitle {
color: var(--shadow);
font-size: 1rem;
}
.album-toggle { .album-toggle {
height: fit-content; height: fit-content;
display: inline-flex; display: inline-flex;

View File

@ -12,18 +12,34 @@
:key="sfw" :key="sfw"
class="nolist tiles" class="nolist tiles"
> >
<template v-for="(item, index) in items">
<li <li
v-for="(release, index) in releases" v-if="item === 'campaign'"
:key="`release-${release.id}`" :key="`campaign-${index}`"
class="campaign"
>
<Campaign
v-if="item === 'campaign'"
:entity="entity"
:min-ratio="0.75"
:max-ratio="1.25"
:allow-generic="true"
/>
</li>
<li
v-else
:key="`release-${item.id}`"
> >
<SceneTile <SceneTile
:release="release" :release="item"
:referer="referer" :referer="referer"
:index="index" :index="index"
:stash="stash" :stash="stash"
@stash="isStashed => $emit('stash', isStashed)" @stash="isStashed => $emit('stash', isStashed)"
/> />
</li> </li>
</template>
</ul> </ul>
<span <span
@ -38,28 +54,36 @@
</div> </div>
</template> </template>
<script> <script setup>
import config from 'config';
import {
defineProps,
defineEmits,
computed,
} from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import Campaign from '../campaigns/campaign.vue';
import Ellipsis from '../loading/ellipsis.vue'; import Ellipsis from '../loading/ellipsis.vue';
import SceneTile from './scene-tile.vue'; import SceneTile from './scene-tile.vue';
function range() { const router = useRouter();
return this.$route.params.range; const store = useStore();
}
function sfw() { defineEmits(['stash']);
return this.$store.state.ui.sfw;
}
export default { const props = defineProps({
components: {
Ellipsis,
SceneTile,
},
props: {
releases: { releases: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
entity: {
type: Object,
default: null,
},
context: { context: {
type: String, type: String,
default: null, default: null,
@ -76,13 +100,14 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
}, });
emits: ['stash'],
computed: { const campaignIndex = computed(() => Math.floor(Math.random() * props.releases.length - 5) + 5);
range,
sfw, const items = computed(() => props.releases.flatMap((release, index) => (config.campaigns.tiles && props.releases.length > 10 && index === campaignIndex.value ? ['campaign', release] : release)));
},
}; const range = computed(() => router.route?.params.range);
const sfw = computed(() => store.state.ui.sfw);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -126,6 +151,12 @@ export default {
font-weight: bold; font-weight: bold;
} }
.campaign {
display: flex;
align-items: flex-start;
justify-content: center;
}
@media(max-width: $breakpoint-mega) { @media(max-width: $breakpoint-mega) {
.tiles { .tiles {
grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));

View File

@ -100,10 +100,7 @@
>{{ release.entity.name }}</h3> >{{ release.entity.name }}</h3>
</a> </a>
<span <span class="row">
v-if="release.actors?.length > 0"
class="row"
>
<ul <ul
class="actors nolist" class="actors nolist"
:title="release.actors.map(actor => actor.name).join(', ')" :title="release.actors.map(actor => actor.name).join(', ')"
@ -357,9 +354,9 @@ export default {
} }
.actors { .actors {
height: 1.5rem;
word-wrap: break-word; word-wrap: break-word;
overflow: hidden; overflow: hidden;
max-height: 1.5rem;
line-height: 1.5rem; line-height: 1.5rem;
margin: 0 0 .25rem 0; margin: 0 0 .25rem 0;
} }

View File

@ -28,7 +28,12 @@
</template> </template>
<span <span
v-if="!loading && actors.length === 0 && releases.length === 0" v-if="error"
class="error summary"
>{{ error }}</span>
<span
v-else-if="!loading && actors.length === 0 && releases.length === 0"
class="summary" class="summary"
>No results</span> >No results</span>
</div> </div>
@ -39,6 +44,7 @@ import Actor from '../actors/tile.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
async function search() { async function search() {
try {
const results = await this.$store.dispatch('search', { const results = await this.$store.dispatch('search', {
query: this.query, query: this.query,
limit: 10, limit: 10,
@ -50,6 +56,12 @@ async function search() {
this.actors = results.actors; this.actors = results.actors;
this.releases = results.releases; this.releases = results.releases;
} }
} catch (error) {
this.loading = false;
this.error = 'Failed to retrieve search results, sorry about that.';
console.error(error);
}
} }
function query() { function query() {
@ -101,6 +113,10 @@ export default {
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
color: var(--shadow); color: var(--shadow);
font-weight: bold; font-weight: bold;
&.error {
color: var(--error);
}
} }
.tiles { .tiles {

View File

@ -42,7 +42,7 @@ export default {
}, },
data() { data() {
return { return {
tags: ['anal', 'gay', 'transsexual', 'bisexual', 'pissing', 'anal prolapse'], tags: ['gay', 'transsexual', 'bisexual', 'anal', 'anal prolapse', 'pissing'],
}; };
}, },
computed: { computed: {

View File

@ -7,6 +7,11 @@
class="dialog-body" class="dialog-body"
@submit.prevent="addStash" @submit.prevent="addStash"
> >
<div
v-if="errorMsg"
class="form-error"
>{{ errorMsg }}</div>
<input <input
ref="name" ref="name"
v-model="name" v-model="name"
@ -27,11 +32,17 @@
<script> <script>
async function addStash() { async function addStash() {
this.errorMsg = null;
try {
await this.$store.dispatch('createStash', { await this.$store.dispatch('createStash', {
name: this.name, name: this.name,
}); });
this.$emit('close', true); this.$emit('close', true);
} catch (error) {
this.errorMsg = error.message;
}
} }
function mounted() { function mounted() {
@ -39,15 +50,22 @@ function mounted() {
} }
export default { export default {
emits: ['close'],
data() { data() {
return { return {
errorMsg: null,
name: null, name: null,
}; };
}, },
emits: ['close'],
mounted, mounted,
methods: { methods: {
addStash, addStash,
}, },
}; };
</script> </script>
<style lang="scss" scoped>
.input {
width: 100%;
}
</style>

View File

@ -115,6 +115,8 @@ import Pagination from '../pagination/pagination.vue';
async function fetchStash() { async function fetchStash() {
this.stash = await this.$store.dispatch('fetchStash', { this.stash = await this.$store.dispatch('fetchStash', {
stashId: this.$route.params.stashId, stashId: this.$route.params.stashId,
stashSlug: this.$route.params.stashSlug,
username: this.$route.params.username,
section: this.$route.params.range, section: this.$route.params.range,
pageNumber: this.$route.params.pageNumber || 1, pageNumber: this.$route.params.pageNumber || 1,
limit: this.limit, limit: this.limit,

View File

@ -8,71 +8,84 @@
<dt class="stat-label">Version</dt> <dt class="stat-label">Version</dt>
<dd class="stat-value">{{ version }}</dd> <dd class="stat-value">{{ version }}</dd>
</div> </div>
</dl>
<template v-if="loaded">
<dl class="stat-table">
<div class="stat-row"> <div class="stat-row">
<dt class="stat-label">Content updated</dt> <dt class="stat-label">Content updated</dt>
<dd class="stat-value">{{ formatDate(lastScrape, 'YYYY-MM-DD HH:mm') }}</dd> <dd
class="stat-value"
:title="format(lastScrape, 'yyyy-MM-dd HH:mm')"
>{{ formatDistance(lastScrape, new Date(), { includeSeconds: true }) }} ago</dd>
</div> </div>
</dl> </dl>
<dl class="stat-table"> <dl class="stat-table">
<div class="stat-row"> <div class="stat-row">
<dt class="stat-label">Networks</dt> <dt class="stat-label">Networks</dt>
<dd class="stat-value">{{ totalNetworks }}</dd> <dd class="stat-value">{{ totalNetworks.toLocaleString() }}</dd>
</div> </div>
<div class="stat-row"> <div class="stat-row">
<dt class="stat-label">Channels</dt> <dt class="stat-label">Channels</dt>
<dd class="stat-value">{{ totalChannels }}</dd> <dd class="stat-value">{{ totalChannels.toLocaleString() }}</dd>
</div> </div>
<div class="stat-row"> <div class="stat-row">
<dt class="stat-label">Scenes</dt> <dt class="stat-label">Scenes</dt>
<dd class="stat-value">{{ totalScenes }}</dd> <dd class="stat-value">{{ totalScenes.toLocaleString() }}</dd>
</div> </div>
<div class="stat-row"> <div class="stat-row">
<dt class="stat-label">Movies</dt> <dt class="stat-label">Movies</dt>
<dd class="stat-value">{{ totalMovies }}</dd> <dd class="stat-value">{{ totalMovies.toLocaleString() }}</dd>
</div> </div>
<div class="stat-row"> <div class="stat-row">
<dt class="stat-label">Actors</dt> <dt class="stat-label">Actors</dt>
<dd class="stat-value">{{ totalActors }}</dd> <dd class="stat-value">{{ totalActors.toLocaleString() }}</dd>
</div> </div>
</dl> </dl>
</template>
<Ellipsis v-else />
</div> </div>
<Footer /> <Footer />
</div> </div>
</template> </template>
<script> <script setup>
async function mounted() { import { ref, onMounted } from 'vue';
const stats = await this.$store.dispatch('fetchStats'); import { useStore } from 'vuex';
import { format, formatDistance } from 'date-fns';
this.totalScenes = stats.totalScenes; import Ellipsis from '../loading/ellipsis.vue';
this.totalMovies = stats.totalMovies;
this.totalActors = stats.totalActors;
this.totalNetworks = stats.totalNetworks;
this.totalChannels = stats.totalChannels;
this.lastScrape = stats.lastScrape;
this.version = VERSION; // eslint-disable-line no-undef const store = useStore();
} const version = VERSION; // eslint-disable-line no-undef
export default { const loaded = ref(false);
data() { const totalScenes = ref(0);
return { const totalMovies = ref(0);
totalScenes: 0, const totalActors = ref(0);
totalMovies: 0, const totalNetworks = ref(0);
totalActors: 0, const totalChannels = ref(0);
totalNetworks: 0, const lastScrape = ref(null);
totalChannels: 0,
}; onMounted(async () => {
}, const stats = await store.dispatch('fetchStats');
mounted,
}; totalScenes.value = stats.totalScenes;
totalMovies.value = stats.totalMovies;
totalActors.value = stats.totalActors;
totalNetworks.value = stats.totalNetworks;
totalChannels.value = stats.totalChannels;
lastScrape.value = stats.lastScrape;
loaded.value = true;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -49,11 +49,11 @@ import Campaign from '../campaigns/campaign.vue';
function photos() { function photos() {
if (this.tag.poster && this.$store.state.ui.sfw) { if (this.tag.poster && this.$store.state.ui.sfw) {
return [this.tag.poster].concat(this.tag.photos).map(photo => photo.sfw); return [this.tag.poster].concat(this.tag.photos).map((photo) => photo.sfw);
} }
if (this.$store.state.ui.sfw) { if (this.$store.state.ui.sfw) {
return this.tag.photos.map(photo => photo.sfw); return this.tag.photos.map((photo) => photo.sfw);
} }
if (this.tag.poster) { if (this.tag.poster) {

View File

@ -81,6 +81,7 @@
</template> </template>
<script> <script>
import config from 'config';
import { Converter } from 'showdown'; import { Converter } from 'showdown';
import escapeHtml from '../../../src/utils/escape-html'; import escapeHtml from '../../../src/utils/escape-html';
@ -155,7 +156,7 @@ export default {
releases: null, releases: null,
done: false, done: false,
totalCount: 0, totalCount: 0,
limit: 20, limit: Number(this.$route.query.limit || 30) - config.campaigns.tiles, // reserve one campaign spot
pageTitle: null, pageTitle: null,
hasMedia: false, hasMedia: false,
expanded: false, expanded: false,

View File

@ -125,6 +125,7 @@ function mounted() {
} }
export default { export default {
emits: ['open', 'close'],
data() { data() {
return { return {
opened: false, opened: false,
@ -133,7 +134,6 @@ export default {
arrowOffset: 0, arrowOffset: 0,
}; };
}, },
emits: ['open', 'close'],
mounted, mounted,
methods: { methods: {
calculate, calculate,

View File

@ -93,17 +93,43 @@
</div> </div>
<div <div
v-if="alert.entity" v-if="alert.entities.length > 0"
class="alert-section alert-trigger" class="alert-section alert-trigger"
> >
<h4 class="alert-heading">Channel</h4> <h4 class="alert-heading">{{ alert.entities.length > 1 ? 'Channels' : 'Channel' }}</h4>
<Entity <Entity
v-if="alert.entity" v-for="entity in alert.entities"
:entity="alert.entity" :key="`${alert.id}${entity.id}`"
:entity="entity"
class="entity" class="entity"
/> />
</div> </div>
<div
v-if="alert.matches.length > 0"
class="alert-section alert-trigger"
>
<h4 class="alert-heading">Matches</h4>
<ul class="alert-matches nolist">
<li
v-for="match in alert.matches"
:key="`match-${match.id}`"
class="match"
>
<span
v-if="match.expression.slice(0, 1) === '/' && match.expression.slice(-1) === '/'"
class="match-expression"
><span class="match-slash">/</span>{{ match.expression.slice(1, -1) }}<span class="match-slash">/</span></span>
<span
v-else
class="match-expression"
>{{ match.expression }}</span>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -227,26 +253,34 @@ export default {
} }
.alert-actors, .alert-actors,
.alert-tags { .alert-tags,
.alert-matches {
display: flex; display: flex;
grid-gap: .5rem; grid-gap: .5rem;
} }
.tag { .tag,
.match {
color: var(--shadow-strong); color: var(--shadow-strong);
padding: .5rem; padding: .5rem;
border: solid 1px var(--shadow-hint); border: solid 1px var(--shadow-hint);
font-size: .9rem; font-size: .9rem;
font-weight: bold; font-weight: bold;
}
&:hover { .tag:hover {
cursor: pointer; cursor: pointer;
border: solid 1px var(--primary); border: solid 1px var(--primary);
} }
}
.entity { .entity {
width: 10rem; width: 10rem;
height: 2.5rem; height: 2.5rem;
} }
.match-slash {
padding: 0 .1rem;
color: var(--primary);
font-weight: bold;
}
</style> </style>

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="stash"> <div class="stash">
<div class="stash-section stash-header"> <div class="stash-section stash-header">
<router-link <RouterLink
:to="{ name: 'stash', params: { stashId: stash.id, stashSlug: stash.slug, range: 'scenes', pageNumber: 1 } }" :to="{ name: 'stash', params: { stashId: stash.id, stashSlug: stash.slug, range: 'scenes', pageNumber: 1 } }"
class="stash-link nolink" class="stash-link nolink"
> >
<h4 class="stash-name">{{ stash.name }}</h4> <h4 class="stash-name">{{ stash.name }}</h4>
<span class="stash-more">Browse</span> <span class="stash-more">Browse</span>
</router-link> </RouterLink>
<span class="header-actions noselect"> <span class="header-actions noselect">
<label <label

View File

@ -14,11 +14,13 @@
<div class="section-header"> <div class="section-header">
<h3 class="section-heading">Stashes</h3> <h3 class="section-heading">Stashes</h3>
<Icon <button
icon="plus3" v-if="isMe"
class="header-add" class="button button-secondary header-add"
@click="showAddStash = true" @click="showAddStash = true"
/> >
<Icon icon="plus3" />Add stash
</button>
</div> </div>
<ul class="section-body stashes nolist"> <ul class="section-body stashes nolist">
@ -35,6 +37,7 @@
/> />
</li> </li>
<!--
<li <li
v-if="isMe" v-if="isMe"
class="stashes-stash stashes-add" class="stashes-stash stashes-add"
@ -42,6 +45,7 @@
> >
<Icon icon="plus2" /> <Icon icon="plus2" />
</li> </li>
-->
</ul> </ul>
<AddStash <AddStash
@ -54,11 +58,13 @@
<div class="section-header"> <div class="section-header">
<h3 class="section-heading">Alerts</h3> <h3 class="section-heading">Alerts</h3>
<Icon <button
icon="plus3" v-if="isMe"
class="header-add" class="button button-secondary header-add"
@click="showAddAlert = true" @click="showAddAlert = true"
/> >
<Icon icon="plus3" />Set alert
</button>
</div> </div>
<ul class="section-body alerts nolist"> <ul class="section-body alerts nolist">
@ -74,12 +80,14 @@
/> />
</li> </li>
<!--
<li <li
class="alerts-add" class="alerts-add"
@click="showAddAlert = true" @click="showAddAlert = true"
> >
<Icon icon="plus2" /> <Icon icon="plus2" />
</li> </li>
-->
</ul> </ul>
<AddAlert <AddAlert
@ -186,7 +194,7 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin: 0 0 1rem 0; margin: 0 1rem 1rem 0;
} }
.section-body { .section-body {
@ -201,14 +209,6 @@ export default {
} }
.header-add { .header-add {
height: auto;
padding: .5rem 1rem;
fill: var(--shadow);
&:hover {
fill: var(--primary);
cursor: pointer;
}
} }
.stashes-stash { .stashes-stash {

View File

@ -3,3 +3,11 @@
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
} }
.form-error {
padding: .5rem;
margin-bottom: .5rem;
color: var(--text-light);
background: var(--error);
font-weight: bold;
}

View File

@ -26,6 +26,9 @@
} }
.button { .button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none; border: none;
background: none; background: none;
padding: .5rem; padding: .5rem;
@ -49,6 +52,11 @@
background: var(--shadow-weak); background: var(--shadow-weak);
cursor: default; cursor: default;
} }
.icon {
fill: var(--text-light);
margin-right: .5rem;
}
} }
.button-secondary { .button-secondary {
@ -57,12 +65,21 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
color: var(--text-light); color: var(--text-light);
background: var(--primary); background: var(--primary);
.icon {
fill: var(--text-light);
}
} }
&:disabled { &:disabled {
color: var(--shadow-strong); color: var(--shadow-strong);
cursor: default; cursor: default;
} }
.icon {
fill: var(--primary);
margin-right: .5rem;
}
} }
.album-toggle { .album-toggle {

View File

@ -52,9 +52,10 @@ $breakpoint4: 1500px;
--female: #f0a; --female: #f0a;
--alert: #f00; --alert: #f00;
--error: #f00; --error: #fd5555;
--warn: #fa0; --warn: #fa0;
--success: #5c2; --success: #5c2;
--notice: #25c;
--enabled: #5c2; --enabled: #5c2;
--enabled-background: rgba(0, 255, 0, .1); --enabled-background: rgba(0, 255, 0, .1);

View File

@ -18,6 +18,10 @@
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<script src="/js/bundle.js" defer></script> <script src="/js/bundle.js" defer></script>
<% if (analytics.enabled) { %>
<script async src="<%- analytics.address %>" data-website-id="<%- analytics.siteId %>"></script>
<% } %>
</head> </head>
<body> <body>
<div id="container"></div> <div id="container"></div>

View File

@ -10,6 +10,7 @@ import {
actorStashesFields, actorStashesFields,
getIncludedEntities, getIncludedEntities,
getIncludedActors, getIncludedActors,
batchFragment,
} from '../fragments'; } from '../fragments';
function initActorActions(store, router) { function initActorActions(store, router) {
@ -27,7 +28,7 @@ function initActorActions(store, router) {
const includedTags = router.currentRoute.value.query.tags ? router.currentRoute.value.query.tags.split(',') : []; const includedTags = router.currentRoute.value.query.tags ? router.currentRoute.value.query.tags.split(',') : [];
const mode = router.currentRoute.value.query.mode || 'all'; const mode = router.currentRoute.value.query.mode || 'all';
const { actor } = await graphql(` const { actor, batches: [lastBatch] } = await graphql(`
query Actor( query Actor(
$actorId: Int! $actorId: Int!
$userId: Int, $userId: Int,
@ -36,8 +37,6 @@ function initActorActions(store, router) {
$offset:Int = 0, $offset:Int = 0,
$after:Datetime = "1900-01-01", $after:Datetime = "1900-01-01",
$before:Datetime = "2100-01-01", $before:Datetime = "2100-01-01",
$afterTime:Datetime = "1900-01-01",
$beforeTime:Datetime = "2100-01-01",
$orderBy:[ReleasesOrderBy!] $orderBy:[ReleasesOrderBy!]
$selectableTags: [String], $selectableTags: [String],
$includedTags: [String!], $includedTags: [String!],
@ -52,6 +51,7 @@ function initActorActions(store, router) {
slug slug
realName realName
gender gender
orientation
dateOfBirth dateOfBirth
dateOfDeath dateOfDeath
age age
@ -204,23 +204,10 @@ function initActorActions(store, router) {
} }
scenesConnection( scenesConnection(
filter: { filter: {
or: [ effectiveDate: {
{
date: {
lessThan: $before, lessThan: $before,
greaterThan: $after greaterThan: $after
} }
},
{
date: {
isNull: true
},
createdAt: {
lessThan: $beforeTime,
greaterThan: $afterTime,
}
}
]
and: [ and: [
{ {
or: $includedEntities or: $includedEntities
@ -254,6 +241,7 @@ function initActorActions(store, router) {
} }
${actorStashesFields} ${actorStashesFields}
} }
${batchFragment}
} }
`, { `, {
actorId, actorId,
@ -261,8 +249,6 @@ function initActorActions(store, router) {
offset: Math.max(0, (pageNumber - 1)) * limit, offset: Math.max(0, (pageNumber - 1)) * limit,
after, after,
before, before,
afterTime: store.getters.after,
beforeTime: store.getters.before,
selectableTags: config.selectableTags, selectableTags: config.selectableTags,
orderBy, orderBy,
exclude: store.state.ui.tagFilter, exclude: store.state.ui.tagFilter,
@ -281,7 +267,7 @@ function initActorActions(store, router) {
return { return {
actor: curateActor(actor, null, curateRelease), actor: curateActor(actor, null, curateRelease),
releases: actor.scenesConnection.releases.map((release) => curateRelease(release)), releases: actor.scenesConnection.releases.map((release) => curateRelease(release, 'scene', { lastBatch: lastBatch.id })),
totalCount: actor.scenesConnection.totalCount, totalCount: actor.scenesConnection.totalCount,
}; };
} }

View File

@ -7,6 +7,11 @@ export default {
mediaPath: '/media', mediaPath: '/media',
s3Path: 'https://s3.wasabisys.com', s3Path: 'https://s3.wasabisys.com',
}, },
campaigns: {
home: true,
entity: true,
tiles: 0,
},
showDisclaimer: false, showDisclaimer: false,
disclaimer: 'This site is in early development, and content may occasionally disappear. Please stay tuned, you will be able to use traxxx to its full potential in the near future!', disclaimer: 'This site is in early development, and content may occasionally disappear. Please stay tuned, you will be able to use traxxx to its full potential in the near future!',
selectableTags: [ selectableTags: [

View File

@ -65,20 +65,22 @@ function curateActor(actor, release) {
return curatedActor; return curatedActor;
} }
function curateRelease(release, type = 'scene') { function curateRelease(release, type = 'scene', context = {}) {
const curatedRelease = { const curatedRelease = {
...release, ...release,
type: release.type || type, type: release.type || type,
actors: [], actors: [],
poster: release.poster && release.poster.media, poster: release.poster && release.poster.media,
tags: release.tags ? release.tags.map((tag) => tag.tag || tag) : [], tags: release.tags ? release.tags.map((tag) => tag.tag || tag) : [],
isNew: release.createdBatchId === context.lastBatch,
}; };
curatedRelease.scenes = release.scenes?.filter(Boolean).map(({ scene }) => curateRelease(scene, 'scene')) || []; curatedRelease.scenes = release.scenes?.filter(Boolean).map(({ scene }) => curateRelease(scene, 'scene', context)) || [];
curatedRelease.movies = release.movies?.filter(Boolean).map(({ movie }) => curateRelease(movie, 'movie')) || []; curatedRelease.movies = release.movies?.filter(Boolean).map(({ movie }) => curateRelease(movie, 'movie', context)) || [];
curatedRelease.series = release.series?.filter(Boolean).map(({ serie }) => curateRelease(serie, 'serie')) || []; curatedRelease.series = release.series?.filter(Boolean).map(({ serie }) => curateRelease(serie, 'serie', context)) || [];
curatedRelease.chapters = release.chapters?.filter(Boolean).map((chapter) => curateRelease(chapter)) || []; curatedRelease.chapters = release.chapters?.filter(Boolean).map((chapter) => curateRelease(chapter, 'chapter', context)) || [];
curatedRelease.photos = release.photos?.filter(Boolean).map((photo) => photo.media || photo) || []; curatedRelease.photos = release.photos?.filter(Boolean).map((photo) => photo.media || photo) || [];
curatedRelease.caps = release.caps?.filter(Boolean).map((cap) => cap.media || cap) || [];
curatedRelease.scenesPhotos = release.scenesPhotos?.filter(Boolean).map((photo) => photo.media || photo) || []; curatedRelease.scenesPhotos = release.scenesPhotos?.filter(Boolean).map((photo) => photo.media || photo) || [];
curatedRelease.covers = release.covers?.filter(Boolean).map(({ media }) => media) || []; curatedRelease.covers = release.covers?.filter(Boolean).map(({ media }) => media) || [];
@ -102,7 +104,7 @@ function curateRelease(release, type = 'scene') {
return curatedRelease; return curatedRelease;
} }
function curateEntity(entity, parent, releases) { function curateEntity(entity, parent, releases, context) {
const curatedEntity = { const curatedEntity = {
...entity, ...entity,
children: [], children: [],
@ -120,19 +122,19 @@ function curateEntity(entity, parent, releases) {
} }
if (entity.parent || parent) curatedEntity.parent = curateEntity(entity.parent || parent); if (entity.parent || parent) curatedEntity.parent = curateEntity(entity.parent || parent);
if (releases) curatedEntity.releases = releases.map((release) => curateRelease(release)); if (releases) curatedEntity.releases = releases.map((release) => curateRelease(release, 'scene', context));
curatedEntity.sceneTotal = entity.sceneTotal; curatedEntity.sceneTotal = entity.sceneTotal;
return curatedEntity; return curatedEntity;
} }
function curateTag(tag) { function curateTag(tag, context) {
const curatedTag = { const curatedTag = {
...tag, ...tag,
}; };
if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release)); if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release, 'scene', context));
if (tag.banners) curatedTag.banners = tag.banners.map(({ banner }) => banner); if (tag.banners) curatedTag.banners = tag.banners.map(({ banner }) => banner);
if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media); if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media);
if (tag.poster) curatedTag.poster = tag.poster.media; if (tag.poster) curatedTag.poster = tag.poster.media;
@ -140,14 +142,14 @@ function curateTag(tag) {
return curatedTag; return curatedTag;
} }
function curateStash(stash) { function curateStash(stash, context) {
const curatedStash = stash; const curatedStash = stash;
if (stash.scenes || stash.scenesConnection?.scenes) { if (stash.scenes || stash.scenesConnection?.scenes) {
curatedStash.sceneTotal = stash.scenesConnection?.totalCount || null; curatedStash.sceneTotal = stash.scenesConnection?.totalCount || null;
curatedStash.scenes = (stash.scenesConnection?.scenes || stash.scenes).map((item) => ({ curatedStash.scenes = (stash.scenesConnection?.scenes || stash.scenes).map((item) => ({
...item, ...item,
scene: curateRelease(item.scene), scene: curateRelease(item.scene, 'scene', context),
})); }));
} }
@ -163,7 +165,7 @@ function curateStash(stash) {
curatedStash.movieTotal = stash.moviesConnection?.totalCount || null; curatedStash.movieTotal = stash.moviesConnection?.totalCount || null;
curatedStash.movies = (stash.moviesConnection?.movies || stash.movies).map((item) => ({ curatedStash.movies = (stash.moviesConnection?.movies || stash.movies).map((item) => ({
...item, ...item,
movie: curateRelease(item.movie), movie: curateRelease(item.movie, 'movie', context),
})); }));
} }
@ -189,6 +191,10 @@ function curateAlert(alert) {
curatedAlert.entity = curateEntity(alert.entity.entity || alert.entity); curatedAlert.entity = curateEntity(alert.entity.entity || alert.entity);
} }
if (alert.entities) {
curatedAlert.entities = alert.entities.map((entity) => curateEntity(entity.entity || entity));
}
if (alert.stashes) { if (alert.stashes) {
curatedAlert.stashes = alert.stashes.map((stash) => curateStash(stash.stash || stash)); curatedAlert.stashes = alert.stashes.map((stash) => curateStash(stash.stash || stash));
} }

View File

@ -1,6 +1,6 @@
import { graphql } from '../api'; import { graphql } from '../api';
// import { sitesFragment, releaseFields } from '../fragments'; // import { sitesFragment, releaseFields } from '../fragments';
import { releaseFields, campaignsFragment } from '../fragments'; import { releaseFields, batchFragment, campaignsFragment } from '../fragments';
import { curateEntity } from '../curate'; import { curateEntity } from '../curate';
import getDateRange from '../get-date-range'; import getDateRange from '../get-date-range';
@ -14,7 +14,7 @@ function initEntitiesActions(store, router) {
}) { }) {
const { before, after, orderBy } = getDateRange(range); const { before, after, orderBy } = getDateRange(range);
const { entity } = await graphql(` const { entity, connection, batches: [lastBatch] } = await graphql(`
query Entity( query Entity(
$entitySlug: String! $entitySlug: String!
$entityType: String! = "channel" $entityType: String! = "channel"
@ -22,7 +22,7 @@ function initEntitiesActions(store, router) {
$offset: Int = 0, $offset: Int = 0,
$after: Datetime = "1900-01-01", $after: Datetime = "1900-01-01",
$before: Datetime = "2100-01-01", $before: Datetime = "2100-01-01",
$orderBy: [ReleasesOrderBy!] $orderBy: [ReleasesSummariesOrderBy!]
$exclude: [String!] $exclude: [String!]
$hasAuth: Boolean! $hasAuth: Boolean!
$userId: Int $userId: Int
@ -67,7 +67,6 @@ function initEntitiesActions(store, router) {
independent independent
hasLogo hasLogo
${campaignsFragment} ${campaignsFragment}
sceneTotal
children: childEntitiesConnection { children: childEntitiesConnection {
totalCount totalCount
} }
@ -84,32 +83,52 @@ function initEntitiesActions(store, router) {
hasLogo hasLogo
${campaignsFragment} ${campaignsFragment}
} }
connection: scenesConnection( }
connection: releasesSummariesConnection(
first: $limit first: $limit
offset: $offset offset: $offset
orderBy: $orderBy orderBy: $orderBy
filter: { filter: {
effectiveDate: { effectiveDate: { lessThan: $before, greaterThan: $after }
lessThan: $before, showcased: { equalTo: true }
greaterThan: $after and: [
{
or: [
{
not: { tags: { overlaps: $exclude } }
} }
releasesTagsConnection: { {
none: { tags: { isNull: true }
tag: {
slug: {
in: $exclude
} }
]
} }
{
or: [
{
channelSlug: { equalTo: $entitySlug }
channelType: { equalTo: $entityType }
} }
{
networkSlug: { equalTo: $entitySlug }
networkType: { equalTo: $entityType }
} }
{
parentNetworkSlug: { equalTo: $entitySlug }
parentNetworkType: { equalTo: $entityType }
}
]
}
]
} }
) { ) {
releases: nodes { releases: nodes {
release {
${releaseFields} ${releaseFields}
} }
}
totalCount totalCount
} }
} ${batchFragment}
} }
`, { `, {
entitySlug, entitySlug,
@ -130,8 +149,8 @@ function initEntitiesActions(store, router) {
} }
return { return {
entity: curateEntity(entity, null, entity.connection.releases), entity: curateEntity(entity, null, connection.releases.map(({ release }) => release), { lastBatch: lastBatch?.id }),
totalCount: entity.connection.totalCount, totalCount: connection.totalCount,
}; };
} }

View File

@ -70,6 +70,7 @@ const actorFields = `
ageFromBirth ageFromBirth
dateOfBirth dateOfBirth
gender gender
orientation
avatar: avatarMedia { avatar: avatarMedia {
id id
path path
@ -96,6 +97,13 @@ const actorFields = `
${actorStashesFields} ${actorStashesFields}
`; `;
const basicActorFields = `
id
name
slug
gender
`;
const movieFields = ` const movieFields = `
id id
title title
@ -165,27 +173,7 @@ const movieFields = `
} }
`; `;
const campaignsFragment = ` const campaignFields = `
campaigns(filter: {
or: [
{
banner: {
bannersTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
}
}
}
}
}
}
{
bannerExists: false
}
]
}) {
id id
url url
affiliate { affiliate {
@ -224,6 +212,30 @@ const campaignsFragment = `
slug slug
} }
} }
`;
const campaignsFragment = `
campaigns(filter: {
or: [
{
banner: {
bannersTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
}
}
}
}
}
}
{
bannerExists: false
}
]
}) {
${campaignFields}
} }
`; `;
@ -235,6 +247,14 @@ const releaseActorsFragment = `
} }
`; `;
const releaseBasicActorsFragment = `
actors: releasesActors(orderBy: ACTOR_BY_ACTOR_ID__GENDER_ASC) {
actor {
${basicActorFields}
}
}
`;
const releaseDirectorFragment = ` const releaseDirectorFragment = `
directors: releasesDirectors(orderBy: ACTOR_BY_DIRECTOR_ID__NAME_ASC) { directors: releasesDirectors(orderBy: ACTOR_BY_DIRECTOR_ID__NAME_ASC) {
director { director {
@ -334,6 +354,31 @@ const releasePhotosFragment = `
} }
`; `;
const releaseCapsFragment = `
caps: releasesCaps(orderBy: MEDIA_BY_MEDIA_ID__INDEX_ASC) {
media {
id
index
path
thumbnail
width
height
thumbnailWidth
thumbnailHeight
lazy
isS3
comment
sfw: sfwMedia {
id
thumbnail
lazy
path
comment
}
}
}
`;
const releaseTrailerFragment = ` const releaseTrailerFragment = `
trailer: releasesTrailer { trailer: releasesTrailer {
media { media {
@ -374,11 +419,11 @@ const releaseFields = `
createdAt createdAt
url url
createdBatchId createdBatchId
${releaseActorsFragment} ${releaseBasicActorsFragment}
${releaseTagsFragment} ${releaseTagsFragment}
${releasePosterFragment} ${releasePosterFragment}
${releaseCoversFragment}
${releasePhotosFragment} ${releasePhotosFragment}
${releaseCapsFragment}
${siteFragment} ${siteFragment}
studio { studio {
id id
@ -393,7 +438,6 @@ const releaseFields = `
slug slug
} }
} }
isNew
isFavorited isFavorited
isStashed(includeFavorites: false) isStashed(includeFavorites: false)
stashes: stashesScenesBySceneId( stashes: stashesScenesBySceneId(
@ -413,22 +457,23 @@ const releaseFields = `
} }
} }
`; `;
// isNew too performance-intensive
/*
const releasesFragment = ` const releasesFragment = `
connection: releasesConnection( connection: releasesConnection(
filter: { filter: {
releasesNotShowcasedsConnectionExist: false releasesNotShowcasedsConnectionExist: false
date: { effectiveDate: {
lessThan: $before, lessThan: $before,
greaterThan: $after greaterThan: $after
} }
releasesTagsConnection: { releasesTagsConnection: {
none: { none: {
tag: { tag: {
or: [ slug: {
{ slug: { in: $exclude } } in: $exclude
{ name: { in: $exclude } } }
]
} }
} }
} }
@ -443,6 +488,35 @@ const releasesFragment = `
totalCount totalCount
} }
`; `;
*/
const releasesFragment = `
connection: releasesSummariesConnection(
first: $limit
offset: $offset
orderBy: $orderBy
filter: {
or: [
{
not: { tags: { overlaps: $exclude } }
}
{
tags: { isNull: true }
}
]
effectiveDate: { lessThan: $before, greaterThan: $after }
showcased: { equalTo: true }
batchShowcased: { in: $batchShowcased }
}
) {
releases: nodes {
release {
${releaseFields}
}
}
totalCount
}
`;
const mediaFields = ` const mediaFields = `
id id
@ -471,6 +545,7 @@ const releaseFragment = `
release(id: $releaseId) { release(id: $releaseId) {
id id
title title
altTitles
description description
date date
datePrecision datePrecision
@ -478,6 +553,7 @@ const releaseFragment = `
createdAt createdAt
shootId shootId
qualities qualities
photoCount
productionDate productionDate
createdBatchId createdBatchId
productionLocation productionLocation
@ -495,6 +571,7 @@ const releaseFragment = `
${releaseTagsFragment} ${releaseTagsFragment}
${releasePosterFragment} ${releasePosterFragment}
${releasePhotosFragment} ${releasePhotosFragment}
${releaseCapsFragment}
${releaseCoversFragment} ${releaseCoversFragment}
${releaseTrailerFragment} ${releaseTrailerFragment}
${releaseTeaserFragment} ${releaseTeaserFragment}
@ -536,6 +613,7 @@ const releaseFragment = `
name name
slug slug
url url
showcased
} }
movies: moviesScenesBySceneId { movies: moviesScenesBySceneId {
movie { movie {
@ -596,6 +674,16 @@ const releaseFragment = `
} }
`; `;
const batchFragment = `
batches(
first: 1,
orderBy: CREATED_AT_DESC,
filter: { showcased: { equalTo: true } }
) {
id
}
`;
function getIncludedEntities(router) { function getIncludedEntities(router) {
const includedChannels = router.currentRoute.value.query.channels ? router.currentRoute.value.query.channels.split(',') : []; const includedChannels = router.currentRoute.value.query.channels ? router.currentRoute.value.query.channels.split(',') : [];
const includedNetworks = router.currentRoute.value.query.networks ? router.currentRoute.value.query.networks.split(',') : []; const includedNetworks = router.currentRoute.value.query.networks ? router.currentRoute.value.query.networks.split(',') : [];
@ -662,6 +750,8 @@ function getIncludedActors(router) {
export { export {
actorFields, actorFields,
actorStashesFields, actorStashesFields,
batchFragment,
campaignFields,
campaignsFragment, campaignsFragment,
mediaFields, mediaFields,
mediaFragment, mediaFragment,

View File

@ -12,7 +12,7 @@ const dateRanges = {
upcoming: () => ({ upcoming: () => ({
after: dayjs.utc().toDate(), after: dayjs.utc().toDate(),
before: '2100-01-01', before: '2100-01-01',
orderBy: ['EFFECTIVE_DATE_DESC'], orderBy: ['EFFECTIVE_DATE_ASC'],
}), }),
new: () => ({ new: () => ({
after: '1900-01-01 00:00:00', after: '1900-01-01 00:00:00',

View File

@ -8,6 +8,7 @@ import router from './router';
import initStore from './store'; import initStore from './store';
import initUiObservers from './ui/observers'; import initUiObservers from './ui/observers';
import initAuthObservers from './auth/observers'; import initAuthObservers from './auth/observers';
import setPageTitle from './set-page-title';
import { formatDate, formatDuration } from './format'; import { formatDate, formatDuration } from './format';
@ -101,12 +102,7 @@ async function init() {
}, },
watch: { watch: {
pageTitle(title) { pageTitle(title) {
if (title) { setPageTitle(title); // for Options API components
document.title = `traxxx - ${title}`;
return;
}
document.title = 'traxxx';
}, },
}, },
beforeCreate() { beforeCreate() {

View File

@ -1,7 +1,7 @@
import { graphql } from '../api'; import { graphql } from '../api';
// import { sitesFragment, releaseFields } from '../fragments'; // import { sitesFragment, releaseFields } from '../fragments';
import { releaseFields } from '../fragments'; import { releaseFields } from '../fragments';
import { curateNetwork } from '../curate'; import { curateEntity } from '../curate';
import getDateRange from '../get-date-range'; import getDateRange from '../get-date-range';
function initNetworksActions(store, _router) { function initNetworksActions(store, _router) {
@ -59,23 +59,10 @@ function initNetworksActions(store, _router) {
{ parent: { parent: { slug: { equalTo: $networkSlug } } } } { parent: { parent: { slug: { equalTo: $networkSlug } } } }
] ]
} }
or: [ effectiveDate: {
{
date: {
lessThan: $before, lessThan: $before,
greaterThan: $after greaterThan: $after
} }
},
{
date: {
isNull: true
},
createdAt: {
lessThan: $beforeTime,
greaterThan: $afterTime,
}
}
]
releasesTagsConnection: { releasesTagsConnection: {
none: { none: {
tag: { tag: {
@ -106,7 +93,7 @@ function initNetworksActions(store, _router) {
}); });
return { return {
network: curateNetwork(network, releases), network: curateEntity(network, releases),
totalCount, totalCount,
}; };
} }
@ -131,7 +118,7 @@ function initNetworksActions(store, _router) {
} }
`); `);
return networks.map(network => curateNetwork(network)); return networks.map((network) => curateEntity(network));
} }
return { return {

View File

@ -1,5 +1,6 @@
import { graphql } from '../api'; import { graphql } from '../api';
import { import {
batchFragment,
releasesFragment, releasesFragment,
releaseFragment, releaseFragment,
releaseFields, releaseFields,
@ -14,7 +15,7 @@ function initReleasesActions(store, router) {
async function fetchReleases({ _commit }, { limit = 10, pageNumber = 1, range = 'latest' }) { async function fetchReleases({ _commit }, { limit = 10, pageNumber = 1, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range); const { before, after, orderBy } = getDateRange(range);
const { connection: { releases, totalCount } } = await graphql(` const { connection: { releases, totalCount }, batches: [lastBatch] } = await graphql(`
query Releases( query Releases(
$hasAuth: Boolean! $hasAuth: Boolean!
$userId: Int $userId: Int
@ -22,10 +23,12 @@ function initReleasesActions(store, router) {
$offset:Int = 0, $offset:Int = 0,
$after:Datetime = "1900-01-01 00:00:00", $after:Datetime = "1900-01-01 00:00:00",
$before:Datetime = "2100-01-01 00:00:00", $before:Datetime = "2100-01-01 00:00:00",
$orderBy: [ReleasesOrderBy!], $orderBy: [ReleasesSummariesOrderBy!],
$exclude: [String!] $exclude: [String!],
$batchShowcased: [Boolean!]
) { ) {
${releasesFragment} ${releasesFragment}
${batchFragment}
} }
`, { `, {
hasAuth: !!store.state.auth.user, hasAuth: !!store.state.auth.user,
@ -36,10 +39,11 @@ function initReleasesActions(store, router) {
before, before,
orderBy, orderBy,
exclude: store.state.ui.tagFilter, exclude: store.state.ui.tagFilter,
batchShowcased: range === 'new' ? true : [true, false],
}); });
return { return {
releases: releases.map((release) => curateRelease(release.release || release)), releases: releases.map((release) => curateRelease(release.release || release, 'scene', { lastBatch: lastBatch.id })),
totalCount, totalCount,
}; };
} }
@ -80,12 +84,7 @@ function initReleasesActions(store, router) {
connection: moviesConnection( connection: moviesConnection(
first: $limit first: $limit
offset: $offset offset: $offset
orderBy: DATE_DESC orderBy: EFFECTIVE_DATE_DESC
filter: {
date: {
isNull: false
}
}
) { ) {
movies: nodes { movies: nodes {
${movieFields} ${movieFields}

View File

@ -1,5 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import setPageTitle from './set-page-title';
import Home from '../components/home/home.vue'; import Home from '../components/home/home.vue';
import Login from '../components/auth/login.vue'; import Login from '../components/auth/login.vue';
import Signup from '../components/auth/signup.vue'; import Signup from '../components/auth/signup.vue';
@ -233,7 +235,7 @@ const routes = [
name: 'notifications', name: 'notifications',
}, },
{ {
path: '/stash/:stashId/:stashSlug', path: '/stash/:username/:stashSlug',
redirect: (from) => ({ redirect: (from) => ({
name: 'stash', name: 'stash',
params: { params: {
@ -244,7 +246,7 @@ const routes = [
}), }),
}, },
{ {
path: '/stash/:stashId/:stashSlug?/:range/:pageNumber', path: '/stash/:username/:stashSlug/:range/:pageNumber',
component: Stash, component: Stash,
name: 'stash', name: 'stash',
}, },
@ -257,11 +259,17 @@ const routes = [
path: '/stats', path: '/stats',
component: Stats, component: Stats,
name: 'stats', name: 'stats',
meta: {
title: 'Stats',
},
}, },
{ {
path: '/not-found', path: '/not-found',
name: 'not-found', name: 'not-found',
component: NotFound, component: NotFound,
meta: {
title: 'Not Found',
},
}, },
{ {
path: '/:catchAll(.*)', path: '/:catchAll(.*)',
@ -276,4 +284,6 @@ const router = createRouter({
routes, routes,
}); });
router.beforeEach((to) => setPageTitle(to.meta.title));
export default router; export default router;

View File

@ -0,0 +1,10 @@
function setPageTitle(name) {
if (name) {
document.title = `traxxx - ${name}`;
return;
}
document.title = 'traxxx';
}
export default setPageTitle;

View File

@ -1,6 +1,6 @@
import { graphql } from '../api'; import { graphql } from '../api';
import { releaseFields } from '../fragments'; import { releaseFields } from '../fragments';
import { curateSite, curateRelease } from '../curate'; import { curateEntity, curateRelease } from '../curate';
import getDateRange from '../get-date-range'; import getDateRange from '../get-date-range';
function initSitesActions(store, _router) { function initSitesActions(store, _router) {
@ -41,7 +41,7 @@ function initSitesActions(store, _router) {
} }
releasesConnection( releasesConnection(
filter: { filter: {
date: { effectiveDate: {
lessThan: $before, lessThan: $before,
greaterThan: $after, greaterThan: $after,
}, },
@ -75,7 +75,7 @@ function initSitesActions(store, _router) {
equalTo: $siteSlug equalTo: $siteSlug
} }
} }
date: { effectiveDate: {
lessThan: $before, lessThan: $before,
greaterThan: $after greaterThan: $after
} }
@ -103,13 +103,12 @@ function initSitesActions(store, _router) {
after, after,
before, before,
orderBy, orderBy,
isNew: store.getters.isNew,
exclude: store.state.ui.filter, exclude: store.state.ui.filter,
}); });
return { return {
site: curateSite(site), site: curateEntity(site),
releases: releases.map(release => curateRelease(release)), releases: releases.map((release) => curateRelease(release)),
totalCount, totalCount,
}; };
} }

View File

@ -10,14 +10,16 @@ import { curateStash } from '../curate';
function initStashesActions(store, _router) { function initStashesActions(store, _router) {
async function fetchStash(context, { async function fetchStash(context, {
stashId, stashSlug,
username,
section = 'scenes', section = 'scenes',
pageNumber = 1, pageNumber = 1,
limit = 20, limit = 20,
}) { }) {
const { stash } = await graphql(` const { stashes: [stash] } = await graphql(`
query Stash( query Stash(
$stashId: Int! $stashSlug: String!
$username: String!
$includeScenes: Boolean! $includeScenes: Boolean!
$includeActors: Boolean! $includeActors: Boolean!
$includeMovies: Boolean! $includeMovies: Boolean!
@ -26,7 +28,12 @@ function initStashesActions(store, _router) {
$hasAuth: Boolean! $hasAuth: Boolean!
$userId: Int $userId: Int
) { ) {
stash(id: $stashId) { stashes(
filter: {
user: { username: { equalTo: $username } }
slug: { equalTo: $stashSlug }
}
) {
id id
name name
slug slug
@ -101,7 +108,8 @@ function initStashesActions(store, _router) {
} }
} }
`, { `, {
stashId: Number(stashId), stashSlug,
username,
includeScenes: section === 'scenes', includeScenes: section === 'scenes',
includeActors: section === 'actors', includeActors: section === 'actors',
includeMovies: section === 'movies', includeMovies: section === 'movies',

View File

@ -1,6 +1,7 @@
import { graphql, get } from '../api'; import { graphql, get } from '../api';
import { import {
releaseFields, releaseFields,
batchFragment,
} from '../fragments'; } from '../fragments';
import { curateTag, curateRelease } from '../curate'; import { curateTag, curateRelease } from '../curate';
import getDateRange from '../get-date-range'; import getDateRange from '../get-date-range';
@ -14,14 +15,14 @@ function initTagsActions(store, _router) {
}) { }) {
const { before, after, orderBy } = getDateRange(range); const { before, after, orderBy } = getDateRange(range);
const { tagBySlug } = await graphql(` const { tagBySlug, scenesConnection, batches: [lastBatch] } = await graphql(`
query Tag( query Tag(
$tagSlug: String! $tagSlug: String!
$offset: Int = 0, $offset: Int = 0,
$limit:Int = 1000, $limit:Int = 1000,
$after:Datetime = "1900-01-01", $after:Datetime = "1900-01-01",
$before:Datetime = "2100-01-01", $before:Datetime = "2100-01-01",
$orderBy: [ReleasesOrderBy!], $orderBy: [ReleasesSummariesOrderBy!],
$exclude: [String!] $exclude: [String!]
$hasAuth: Boolean! $hasAuth: Boolean!
$userId: Int $userId: Int
@ -154,32 +155,33 @@ function initTagsActions(store, _router) {
} }
} }
} }
scenesConnection(
filter: {
date: {
lessThan: $before,
greaterThan: $after,
},
releasesTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
} }
} scenesConnection: releasesSummariesConnection(
} first: $limit
}
},
first: $limit,
orderBy: $orderBy,
offset: $offset offset: $offset
orderBy: $orderBy
filter: {
or: [
{
not: { tags: { overlaps: $exclude } }
}
{
tags: { isNull: true }
}
]
tags: { anyEqualTo: $tagSlug }
effectiveDate: { lessThan: $before, greaterThan: $after }
showcased: { equalTo: true }
}
) { ) {
releases: nodes { releases: nodes {
release {
${releaseFields} ${releaseFields}
} }
}
totalCount totalCount
} }
} ${batchFragment}
} }
`, { `, {
tagSlug, tagSlug,
@ -195,8 +197,8 @@ function initTagsActions(store, _router) {
return { return {
tag: curateTag(tagBySlug, null, curateRelease), tag: curateTag(tagBySlug, null, curateRelease),
releases: tagBySlug.scenesConnection.releases.map((release) => curateRelease(release)), releases: scenesConnection.releases.map(({ release }) => curateRelease(release, 'scene', { lastBatch: lastBatch.id })),
totalCount: tagBySlug.scenesConnection.totalCount, totalCount: scenesConnection.totalCount,
}; };
} }

View File

@ -1,5 +1,12 @@
import { graphql, patch } from '../api'; import { graphql, patch } from '../api';
import { releaseFields, actorStashesFields } from '../fragments';
import {
releaseFields,
batchFragment,
campaignFields,
actorStashesFields,
} from '../fragments';
import { curateRelease, curateActor, curateNotification } from '../curate'; import { curateRelease, curateActor, curateNotification } from '../curate';
function initUiActions(store, _router) { function initUiActions(store, _router) {
@ -61,6 +68,7 @@ function initUiActions(store, _router) {
${releaseFields} ${releaseFields}
} }
alert { alert {
all
tags: alertsTags { tags: alertsTags {
tag { tag {
id id
@ -75,7 +83,7 @@ function initUiActions(store, _router) {
slug slug
} }
} }
entity: alertsEntity { entities: alertsEntities {
entity { entity {
id id
name name
@ -83,6 +91,10 @@ function initUiActions(store, _router) {
independent independent
} }
} }
matches: alertsMatches {
property
expression
}
} }
} }
totalCount totalCount
@ -218,6 +230,7 @@ function initUiActions(store, _router) {
} }
${actorStashesFields} ${actorStashesFields}
} }
${batchFragment}
} }
`, { `, {
query, query,
@ -227,7 +240,7 @@ function initUiActions(store, _router) {
}); });
return { return {
releases: res?.results.map((result) => curateRelease(result.release)) || [], releases: res?.results.map((result) => curateRelease(result.release, 'scene', { lastBatch: res?.batches[0].id })) || [],
actors: res?.actors.map((actor) => curateActor(actor)) || [], actors: res?.actors.map((actor) => curateActor(actor)) || [],
}; };
} }
@ -239,29 +252,7 @@ function initUiActions(store, _router) {
$maxRatio: BigFloat $maxRatio: BigFloat
) { ) {
randomCampaign: getRandomCampaign(minRatio: $minRatio, maxRatio: $maxRatio) { randomCampaign: getRandomCampaign(minRatio: $minRatio, maxRatio: $maxRatio) {
url ${campaignFields}
affiliate {
url
}
banner {
id
type
ratio
entity {
type
slug
parent {
type
slug
}
}
}
entity {
slug
}
parent {
slug
}
} }
} }
`, { `, {
@ -272,6 +263,22 @@ function initUiActions(store, _router) {
return randomCampaign; return randomCampaign;
} }
async function fetchCampaign(context, campaignId) {
const { campaign } = await graphql(`
query Campaign(
$campaignId: Int!
) {
campaign(id: $campaignId) {
${campaignFields}
}
}
`, {
campaignId: Number(campaignId),
});
return campaign;
}
async function fetchStats() { async function fetchStats() {
const { const {
scenes, scenes,
@ -279,23 +286,15 @@ function initUiActions(store, _router) {
actors, actors,
networks, networks,
channels, channels,
batches: [batch],
} = await graphql(` } = await graphql(`
query Stats { query Stats {
scenes: releasesConnection( scenes: releasesConnection { totalCount }
last: 1,
orderBy: BATCH_BY_CREATED_BATCH_ID__CREATED_AT_ASC
) {
totalCount
scenes: nodes {
batch: createdBatch {
createdAt
}
}
}
movies: moviesConnection { totalCount } movies: moviesConnection { totalCount }
actors: actorsConnection { totalCount } actors: actorsConnection { totalCount }
networks: entitiesConnection(filter: { type: { equalTo: "network" } }) { totalCount } networks: entitiesConnection(filter: { type: { equalTo: "network" } }) { totalCount }
channels: entitiesConnection(filter: { type: { equalTo: "channel" } }) { totalCount } channels: entitiesConnection(filter: { type: { equalTo: "channel" } }) { totalCount }
batches(orderBy: CREATED_AT_DESC, first: 1) { createdAt }
} }
`); `);
@ -305,7 +304,7 @@ function initUiActions(store, _router) {
totalActors: actors.totalCount, totalActors: actors.totalCount,
totalNetworks: networks.totalCount, totalNetworks: networks.totalCount,
totalChannels: channels.totalCount, totalChannels: channels.totalCount,
lastScrape: new Date(scenes.scenes[0]?.batch.createdAt), lastScrape: new Date(batch.createdAt),
}; };
} }
@ -319,6 +318,7 @@ function initUiActions(store, _router) {
setBatch, setBatch,
setSfw, setSfw,
setTheme, setTheme,
fetchCampaign,
fetchRandomCampaign, fetchRandomCampaign,
fetchNotifications, fetchNotifications,
fetchStats, fetchStats,

View File

@ -71,6 +71,7 @@ function initUsersActions(store, _router) {
id id
notify notify
email email
all
stashes: alertsStashes { stashes: alertsStashes {
stash { stash {
id id
@ -85,12 +86,17 @@ function initUsersActions(store, _router) {
slug slug
} }
} }
matches: alertsMatches {
id
property
expression
}
actors: alertsActors { actors: alertsActors {
actor { actor {
${actorFields} ${actorFields}
} }
} }
entity: alertsEntity { entities: alertsEntities {
entity { entity {
id id
name name

View File

@ -12,6 +12,8 @@ module.exports = {
password: 'password', password: 'password',
database: 'traxxx', database: 'traxxx',
}, },
timeout: 5000,
graphiql: false,
}, },
web: { web: {
host: '0.0.0.0', host: '0.0.0.0',
@ -28,6 +30,20 @@ module.exports = {
}, },
}, },
}, },
redis: {
host: 'localhost',
port: 6379,
username: null,
password: null,
},
location: {
userAgent: 'contact via https://traxxx.me/',
},
analytics: {
enabled: false,
address: 'http://localhost:3000/script.js',
siteId: '1b28ac3b-d229-43bf-aec9-75cf0a72a466',
},
s3: { s3: {
enabled: false, enabled: false,
bucket: 'traxxx', bucket: 'traxxx',
@ -37,6 +53,8 @@ module.exports = {
auth: { auth: {
login: true, login: true,
signup: true, signup: true,
usernameLength: [2, 24],
usernamePattern: /^[a-zA-Z0-9_-]+$/,
}, },
exclude: { exclude: {
channels: [ channels: [
@ -53,6 +71,7 @@ module.exports = {
'amberathome', 'amberathome',
'marycarey', 'marycarey',
'racqueldevonshire', 'racqueldevonshire',
'aziani',
// blowpass // blowpass
'sunlustxxx', 'sunlustxxx',
// ddfnetwork // ddfnetwork
@ -63,6 +82,10 @@ module.exports = {
'lowartfilms', 'lowartfilms',
// freeones // freeones
'freeones', 'freeones',
// new sesations
'tabutales',
'talesfromtheedge',
'shanedieselsbangingbabes',
// pornpros // pornpros
'milfhumiliation', 'milfhumiliation',
'humiliated', 'humiliated',
@ -254,18 +277,21 @@ module.exports = {
'www.tushyraw.com', 'www.tushyraw.com',
'www.deeper.com', 'www.deeper.com',
'www.slayed.com', 'www.slayed.com',
'www.milfy.com',
'sthw-trailer-vixen.ssl-cdn.com', 'sthw-trailer-vixen.ssl-cdn.com',
'sthw-trailer-tushy.ssl-cdn.com', 'sthw-trailer-tushy.ssl-cdn.com',
'sthw-trailer-tushyraw.ssl-cdn.com', 'sthw-trailer-tushyraw.ssl-cdn.com',
'sthw-trailer-blacked.ssl-cdn.com', 'sthw-trailer-blacked.ssl-cdn.com',
'sthw-trailer-blackedraw.ssl-cdn.com', 'sthw-trailer-blackedraw.ssl-cdn.com',
'sthw-trailer-deeper.ssl-cdn.com', 'sthw-trailer-deeper.ssl-cdn.com',
'sthw-trailer-milfy.ssl-cdn.com',
'streamhw-trailer-vixen.ssl-cdn.com', 'streamhw-trailer-vixen.ssl-cdn.com',
'streamhw-trailer-tushy.ssl-cdn.com', 'streamhw-trailer-tushy.ssl-cdn.com',
'streamhw-trailer-tushyraw.ssl-cdn.com', 'streamhw-trailer-tushyraw.ssl-cdn.com',
'streamhw-trailer-blacked.ssl-cdn.com', 'streamhw-trailer-blacked.ssl-cdn.com',
'streamhw-trailer-blackedraw.ssl-cdn.com', 'streamhw-trailer-blackedraw.ssl-cdn.com',
'streamhw-trailer-deeper.ssl-cdn.com', 'streamhw-trailer-deeper.ssl-cdn.com',
'streamhw-trailer-milfy.ssl-cdn.com',
'cdn.vixen.com', 'cdn.vixen.com',
'cdn.tushy.com', 'cdn.tushy.com',
'cdn.blacked.com', 'cdn.blacked.com',
@ -273,6 +299,7 @@ module.exports = {
'cdn.blackedraw.com', 'cdn.blackedraw.com',
'cdn.tushyraw.com', 'cdn.tushyraw.com',
'cdn.slayed.com', 'cdn.slayed.com',
'cdn.milfy.com',
'www.vogov.com', 'www.vogov.com',
'www.vogov.com', 'www.vogov.com',
'www.nubiles.net', 'www.nubiles.net',
@ -297,6 +324,7 @@ module.exports = {
enable: false, enable: false,
hostnames: [ // these can run in the same browser session hostnames: [ // these can run in the same browser session
'www.kink.com', 'www.kink.com',
'store2.psmcdn.net', // Team Skeet API
], ],
}, },
cloudflare: { cloudflare: {
@ -337,14 +365,20 @@ module.exports = {
}, },
media: { media: {
path: './media', path: './media',
transferSources: {
local: 'http://localhost:5000/media',
s3: 'https://cdn.traxxx.me',
},
maxSize: 1000, maxSize: 1000,
quality: 80, quality: 80,
thumbnailSize: 320, // width for 16:9 will be exactly 576px thumbnailSize: 320, // width for 16:9 will be exactly 576px
thumbnailQuality: 100, thumbnailQuality: 100,
lazySize: 90, lazySize: 90,
lazyQuality: 90, lazyQuality: 90,
trailerQuality: [480, 540, 360, 720, 960, 1080, 320, 1440, 1600, 1920, 2160, 270, 240, 180], trailerQuality: [540, 720, 480, 360, 960, 1080, 320, 1440, 1600, 1920, 2160, 270, 240, 180],
limit: 25, // max number of photos per release limit: 25, // max number of photos per release
attempts: 2,
fetchStreams: true,
streamConcurrency: 2, // max number of video streams (m3u8 etc.) to fetch and process at once streamConcurrency: 2, // max number of video streams (m3u8 etc.) to fetch and process at once
}, },
titleSlugLength: 50, titleSlugLength: 50,

28
ecosystem.config.js Normal file
View File

@ -0,0 +1,28 @@
const config = require('config');
module.exports = {
apps: [
{
name: 'traxxx',
script: 'src/init.js',
exec_mode: 'cluster',
instances: 2,
restart_delay: 3000,
args: '--server',
merge_logs: true,
env: {
NODE_ENV: 'production',
},
},
...(config.analytics.enabled ? [{
name: 'umami',
script: 'npm start',
cwd: process.env.UMAMI_DIR || '../umami',
restart_delay: 3000,
merge_logs: true,
env: {
NODE_ENV: 'production',
},
}] : []),
],
};

View File

@ -64,6 +64,7 @@ exports.up = (knex) => Promise.resolve()
table.boolean('independent') table.boolean('independent')
.defaultTo(false); .defaultTo(false);
table.boolean('showcased');
table.boolean('visible') table.boolean('visible')
.defaultTo(true); .defaultTo(true);
@ -272,6 +273,7 @@ exports.up = (knex) => Promise.resolve()
table.integer('age', 3); table.integer('age', 3);
table.text('gender', 18); table.text('gender', 18);
table.text('orientation');
table.text('description'); table.text('description');
table.text('birth_city'); table.text('birth_city');
@ -345,6 +347,7 @@ exports.up = (knex) => Promise.resolve()
table.text('real_name'); table.text('real_name');
table.text('gender', 18); table.text('gender', 18);
table.text('orientation');
table.date('date_of_birth'); table.date('date_of_birth');
table.date('date_of_death'); table.date('date_of_death');
@ -651,6 +654,8 @@ exports.up = (knex) => Promise.resolve()
table.integer('duration') table.integer('duration')
.unsigned(); .unsigned();
table.specificType('qualities', 'text[]');
table.boolean('deep'); table.boolean('deep');
table.text('deep_url', 1000); table.text('deep_url', 1000);
@ -804,6 +809,8 @@ exports.up = (knex) => Promise.resolve()
table.text('original_tag'); table.text('original_tag');
table.unique(['tag_id', 'release_id']); table.unique(['tag_id', 'release_id']);
table.index('tag_id');
table.index('release_id');
})) }))
.then(() => knex.schema.createTable('releases_search', (table) => { .then(() => knex.schema.createTable('releases_search', (table) => {
table.integer('release_id', 16) table.integer('release_id', 16)
@ -873,6 +880,8 @@ exports.up = (knex) => Promise.resolve()
table.datetime('created_at') table.datetime('created_at')
.defaultTo(knex.fn.now()); .defaultTo(knex.fn.now());
table.index('scene_id');
})) }))
.then(() => knex.schema.createTable('movies_covers', (table) => { .then(() => knex.schema.createTable('movies_covers', (table) => {
table.integer('movie_id', 16) table.integer('movie_id', 16)
@ -916,12 +925,151 @@ exports.up = (knex) => Promise.resolve()
table.unique('movie_id'); 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) => { .then(() => knex.schema.createTable('movies_search', (table) => {
table.integer('movie_id', 16) table.integer('movie_id', 16)
.references('id') .references('id')
.inTable('movies') .inTable('movies')
.onDelete('cascade'); .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) => { .then(() => knex.schema.createTable('chapters', (table) => {
table.increments('id', 16); table.increments('id', 16);
@ -1062,6 +1210,8 @@ exports.up = (knex) => Promise.resolve()
table.datetime('created_at') table.datetime('created_at')
.notNullable() .notNullable()
.defaultTo(knex.fn.now()); .defaultTo(knex.fn.now());
table.datetime('last_login');
})) }))
.then(() => knex.schema.createTable('stashes', (table) => { .then(() => knex.schema.createTable('stashes', (table) => {
table.increments('id'); table.increments('id');
@ -1088,7 +1238,12 @@ exports.up = (knex) => Promise.resolve()
table.datetime('created_at') table.datetime('created_at')
.notNullable() .notNullable()
.defaultTo(knex.fn.now()); .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) => { .then(() => knex.schema.createTable('stashes_scenes', (table) => {
table.integer('stash_id') table.integer('stash_id')
.notNullable() .notNullable()
@ -1152,6 +1307,27 @@ exports.up = (knex) => Promise.resolve()
.notNullable() .notNullable()
.defaultTo(knex.fn.now()); .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) => { .then(() => knex.schema.createTable('alerts', (table) => {
table.increments('id'); table.increments('id');
@ -1329,6 +1505,9 @@ exports.up = (knex) => Promise.resolve()
.notNullable() .notNullable()
.defaultTo(knex.fn.now()); .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) => { .then(() => knex.schema.createTable('banners_tags', (table) => {
table.increments('id'); table.increments('id');
@ -1372,17 +1551,46 @@ exports.up = (knex) => Promise.resolve()
.notNullable() .notNullable()
.defaultTo(knex.fn.now()); .defaultTo(knex.fn.now());
})) }))
.then(() => knex.schema.createTable('random_campaign', (table) => {
table.integer('id')
.notNullable()
.references('id')
.inTable('campaigns');
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 // SEARCH AND SORT
.then(() => { // eslint-disable-line arrow-body-style .then(() => { // eslint-disable-line arrow-body-style
// allow vim fold // allow vim fold
return knex.raw(` return knex.raw(`
ALTER TABLE releases_search ADD COLUMN document tsvector; ALTER TABLE releases_search ADD COLUMN document tsvector;
ALTER TABLE movies_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 */ /* allow scenes without dates to be mixed inbetween scenes with dates */
ALTER TABLE releases ALTER TABLE releases
ADD COLUMN effective_date timestamptz ADD COLUMN effective_date timestamptz
GENERATED ALWAYS AS (COALESCE(date, created_at)) STORED; GENERATED ALWAYS AS (COALESCE(date, created_at)) STORED;
ALTER TABLE movies
ADD COLUMN effective_date timestamptz
GENERATED ALWAYS AS (COALESCE(date, created_at)) STORED;
`); `);
}) })
// INDEXES // INDEXES
@ -1401,6 +1609,8 @@ exports.up = (knex) => Promise.resolve()
CREATE UNIQUE INDEX movies_search_unique ON movies_search (movie_id); CREATE UNIQUE INDEX movies_search_unique ON movies_search (movie_id);
CREATE INDEX releases_search_index ON releases_search USING GIN (document); CREATE INDEX releases_search_index ON releases_search USING GIN (document);
CREATE INDEX movies_search_index ON movies_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 // FUNCTIONS
@ -1421,15 +1631,13 @@ exports.up = (knex) => Promise.resolve()
CREATE TABLE movies_search_results (movie_id integer, rank real, FOREIGN KEY (movie_id) REFERENCES movies (id)); 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 $$ CREATE FUNCTION search_releases(query text) RETURNS SETOF releases_search_results AS $$
SELECT releases.id, ranks.rank FROM ( SELECT results.release_id, ts_rank(results.document::tsvector, curate_search_query(query)) as rank
SELECT FROM (
releases_search.release_id, SELECT releases_search.release_id, document
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 FROM releases_search
) ranks WHERE document::tsvector @@ curate_search_query(query)
LEFT JOIN releases ON releases.id = ranks.release_id ) AS results
WHERE ranks.rank > 0 ORDER BY rank DESC;
ORDER BY ranks.rank DESC;
$$ LANGUAGE SQL STABLE; $$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_movies(query text) RETURNS SETOF movies_search_results AS $$ CREATE FUNCTION search_movies(query text) RETURNS SETOF movies_search_results AS $$
@ -1513,6 +1721,52 @@ exports.up = (knex) => Promise.resolve()
ORDER BY actors.name; ORDER BY actors.name;
$$ LANGUAGE SQL STABLE; $$ 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, /* 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 */ 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 $$ CREATE FUNCTION actors_scenes(actor actors, selected_tags text[], mode text DEFAULT 'all') RETURNS SETOF releases AS $$
@ -1589,7 +1843,7 @@ exports.up = (knex) => Promise.resolve()
ORDER BY tags.priority DESC ORDER BY tags.priority DESC
$$ LANGUAGE SQL STABLE; $$ 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.* SELECT media.*
FROM movies_scenes FROM movies_scenes
LEFT JOIN LEFT JOIN
@ -1607,15 +1861,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); SELECT EXISTS(SELECT true WHERE (SELECT id FROM batches ORDER BY created_at DESC LIMIT 1) = release.created_batch_id);
$$ LANGUAGE sql STABLE; $$ LANGUAGE sql STABLE;
CREATE FUNCTION banners_ratio(banner banners) RETURNS numeric AS $$ CREATE FUNCTION series_actors(serie series) RETURNS SETOF actors AS $$
SELECT ROUND(banner.width::decimal / banner.height::decimal, 2); 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; $$ LANGUAGE SQL STABLE;
CREATE FUNCTION get_random_campaign() RETURNS SETOF campaigns AS $$ CREATE FUNCTION series_tags(serie series) RETURNS SETOF tags AS $$
SELECT * FROM campaigns SELECT tags.*
ORDER BY random() 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)
id, 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; 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 // POLICIES
@ -1632,6 +1982,7 @@ exports.up = (knex) => Promise.resolve()
ALTER TABLE stashes_scenes ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes_scenes ENABLE ROW LEVEL SECURITY;
ALTER TABLE stashes_movies ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes_movies ENABLE ROW LEVEL SECURITY;
ALTER TABLE stashes_actors 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_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()); CREATE POLICY stashes_policy_update ON stashes FOR UPDATE USING (stashes.public OR stashes.user_id = current_user_id());
@ -1662,6 +2013,14 @@ exports.up = (knex) => Promise.resolve()
AND (stashes.user_id = current_user_id() OR stashes.public) 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 ENABLE ROW LEVEL SECURITY;
ALTER TABLE alerts_tags ENABLE ROW LEVEL SECURITY; ALTER TABLE alerts_tags ENABLE ROW LEVEL SECURITY;
ALTER TABLE alerts_scenes ENABLE ROW LEVEL SECURITY; ALTER TABLE alerts_scenes ENABLE ROW LEVEL SECURITY;
@ -1729,33 +2088,6 @@ exports.up = (knex) => Promise.resolve()
`, { `, {
visitor: knex.raw(config.database.query.user), 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 exports.down = (knex) => { // eslint-disable-line arrow-body-style
@ -1776,8 +2108,17 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS movies_scenes CASCADE; DROP TABLE IF EXISTS movies_scenes CASCADE;
DROP TABLE IF EXISTS movies_covers CASCADE; DROP TABLE IF EXISTS movies_covers CASCADE;
DROP TABLE IF EXISTS movies_posters 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 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_tags CASCADE;
DROP TABLE IF EXISTS clips_posters CASCADE; DROP TABLE IF EXISTS clips_posters CASCADE;
DROP TABLE IF EXISTS clips_photos CASCADE; DROP TABLE IF EXISTS clips_photos CASCADE;
@ -1789,6 +2130,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS banners_tags CASCADE; DROP TABLE IF EXISTS banners_tags CASCADE;
DROP TABLE IF EXISTS banners CASCADE; DROP TABLE IF EXISTS banners CASCADE;
DROP TABLE IF EXISTS campaigns CASCADE; DROP TABLE IF EXISTS campaigns CASCADE;
DROP TABLE IF EXISTS random_campaign CASCADE;
DROP TABLE IF EXISTS affiliates CASCADE; DROP TABLE IF EXISTS affiliates CASCADE;
DROP TABLE IF EXISTS batches CASCADE; DROP TABLE IF EXISTS batches CASCADE;
@ -1812,6 +2154,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS movies CASCADE; DROP TABLE IF EXISTS movies CASCADE;
DROP TABLE IF EXISTS clips CASCADE; DROP TABLE IF EXISTS clips CASCADE;
DROP TABLE IF EXISTS chapters CASCADE; DROP TABLE IF EXISTS chapters CASCADE;
DROP TABLE IF EXISTS series CASCADE;
DROP TABLE IF EXISTS releases CASCADE; DROP TABLE IF EXISTS releases CASCADE;
DROP TABLE IF EXISTS actors CASCADE; DROP TABLE IF EXISTS actors CASCADE;
DROP TABLE IF EXISTS tags CASCADE; DROP TABLE IF EXISTS tags CASCADE;
@ -1853,6 +2196,9 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP FUNCTION IF EXISTS get_random_sfw_media_id; DROP FUNCTION IF EXISTS get_random_sfw_media_id;
DROP FUNCTION IF EXISTS releases_is_new; 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_tags;
DROP FUNCTION IF EXISTS actors_channels; DROP FUNCTION IF EXISTS actors_channels;
DROP FUNCTION IF EXISTS actors_actors; DROP FUNCTION IF EXISTS actors_actors;
@ -1860,7 +2206,11 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP FUNCTION IF EXISTS movies_actors; DROP FUNCTION IF EXISTS movies_actors;
DROP FUNCTION IF EXISTS movies_tags; 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;
DROP POLICY IF EXISTS stashes_policy ON stashes_scenes; DROP POLICY IF EXISTS stashes_policy ON stashes_scenes;

View File

@ -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;
`);

View File

@ -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');
}));

View File

@ -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;
`);

View File

@ -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;
`);

View File

@ -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;
`));

View File

@ -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;
`);

View File

@ -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');
});

View File

@ -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');
});

View File

@ -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;
`);

View File

@ -1,8 +0,0 @@
exports.up = async (knex) => knex.schema.alterTable('entities', (table) => {
table.boolean('showcased')
.defaultTo(true);
});
exports.down = async (knex) => knex.schema.alterTable('entities', (table) => {
table.dropColumn('showcased');
});

View File

@ -1,20 +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 ON entities.id = releases.entity_id
LEFT JOIN entities AS studios ON studios.id = releases.studio_id
WHERE entities.showcased = false
OR studios.showcased = false
);
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;
`);

View File

@ -0,0 +1,58 @@
exports.up = async (knex) => {
await knex.schema.alterTable('random_campaign', (table) => {
table.integer('id')
.notNullable()
.references('id')
.inTable('campaigns');
});
await knex.raw(`
CREATE OR REPLACE 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, 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) => {
await knex.schema.alterTable('random_campaign', (table) => {
table.dropColumn('campaign_id');
});
await knex.raw(`
CREATE OR REPLACE 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;
`);
};

View File

@ -0,0 +1,47 @@
const config = require('config');
exports.up = async (knex) => {
await knex.raw(`
CREATE MATERIALIZED VIEW releases_summaries AS (
SELECT
releases.id as release_id,
channels.slug as channel_slug,
channels.type as channel_type,
networks.slug as network_slug,
networks.type as network_type,
parent_networks.slug as parent_network_slug,
parent_networks.type as parent_network_type,
studios.showcased IS NOT false
AND (channels.showcased IS NOT false OR COALESCE(studios.showcased, false) = true)
AND (networks.showcased IS NOT false OR COALESCE(channels.showcased, false) = true OR COALESCE(studios.showcased, false) = true)
AS showcased,
batches.showcased AS batch_showcased,
releases.effective_date,
releases.created_at,
array_agg(tags.slug ORDER BY tags.priority DESC) FILTER (WHERE tags.slug IS NOT NULL) AS tags
FROM releases
LEFT JOIN releases_tags ON releases_tags.release_id = releases.id
LEFT JOIN tags ON tags.id = releases_tags.tag_id
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
LEFT JOIN entities AS parent_networks ON parent_networks.id = networks.parent_id
LEFT JOIN batches ON batches.id = releases.updated_batch_id
GROUP BY releases.id, studios.showcased, batches.showcased,
channels.showcased, channels.slug, channels.type,
networks.showcased, networks.slug, networks.type,
parent_networks.slug, parent_networks.type
);
COMMENT ON MATERIALIZED VIEW releases_summaries IS E'@foreignKey (release_id) references releases (id)';
GRANT ALL ON releases_summaries TO :visitor;
`, {
visitor: knex.raw(config.database.query.user),
});
};
exports.down = async (knex) => {
await knex.raw(`
DROP MATERIALIZED VIEW IF EXISTS releases_summaries;
`);
};

View File

@ -0,0 +1,27 @@
exports.up = async (knex) => {
await knex.schema.alterTable('actors_social', (table) => {
table.integer('profile_id')
.references('id')
.inTable('actors_profiles');
table.dropUnique(['url', 'actor_id']);
table.unique(['url', 'actor_id', 'profile_id']);
});
await knex.raw(`
CREATE UNIQUE INDEX actors_social_url_actor_id_null_unique ON actors_social (url, actor_id) WHERE profile_id IS NULL;
`);
};
exports.down = async (knex) => {
await knex.raw(`
DROP INDEX actors_social_url_actor_id_null_unique;
`);
await knex.schema.alterTable('actors_social', (table) => {
table.dropUnique(['url', 'actor_id', 'profile_id']);
table.unique(['url', 'actor_id']);
table.dropColumn('profile_id');
});
};

View File

@ -0,0 +1,24 @@
const config = require('config');
exports.up = async (knex) => {
await knex.schema.createTable('releases_caps', (table) => {
table.integer('release_id')
.notNullable()
.references('id')
.inTable('releases')
.onDelete('cascade');
table.text('media_id')
.notNullable()
.references('id')
.inTable('media');
});
await knex.raw('GRANT ALL ON releases_caps TO :visitor;', {
visitor: knex.raw(config.database.query.user),
});
};
exports.down = async (knex) => {
await knex.schema.dropTable('releases_caps');
};

View File

@ -0,0 +1,27 @@
exports.up = async (knex) => {
await knex.schema.alterTable('actors_profiles', (table) => {
table.string('hair_type');
table.decimal('shoe_size');
table.string('blood_type');
});
await knex.schema.alterTable('actors', (table) => {
table.string('hair_type');
table.decimal('shoe_size');
table.string('blood_type');
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('actors_profiles', (table) => {
table.dropColumn('hair_type');
table.dropColumn('shoe_size');
table.dropColumn('blood_type');
});
await knex.schema.alterTable('actors', (table) => {
table.dropColumn('hair_type');
table.dropColumn('shoe_size');
table.dropColumn('blood_type');
});
};

View File

@ -0,0 +1,13 @@
exports.up = async (knex) => {
await knex.schema.alterTable('batches', (table) => {
table.boolean('showcased')
.notNullable()
.defaultTo(true);
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('batches', (table) => {
table.dropColumn('showcased');
});
};

View File

@ -0,0 +1,27 @@
exports.up = async (knex) => {
await knex.schema.alterTable('releases', (table) => {
table.specificType('alt_titles', 'text ARRAY');
});
await knex.schema.alterTable('movies', (table) => {
table.specificType('alt_titles', 'text ARRAY');
});
await knex.schema.alterTable('series', (table) => {
table.specificType('alt_titles', 'text ARRAY');
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('releases', (table) => {
table.dropColumn('alt_titles');
});
await knex.schema.alterTable('movies', (table) => {
table.dropColumn('alt_titles');
});
await knex.schema.alterTable('series', (table) => {
table.dropColumn('alt_titles');
});
};

View File

@ -0,0 +1,36 @@
exports.up = async (knex) => {
await knex.schema.createTable('movies_teasers', (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');
});
await knex.schema.createTable('series_teasers', (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');
});
};
exports.down = async (knex) => {
await knex.schema.dropTable('movies_teasers');
await knex.schema.dropTable('series_teasers');
};

View File

@ -0,0 +1,19 @@
exports.up = async (knex) => {
await knex.schema.alterTable('releases', (table) => {
table.integer('photo_count');
});
await knex.schema.alterTable('movies', (table) => {
table.integer('photo_count');
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('releases', (table) => {
table.dropColumn('photo_count');
});
await knex.schema.alterTable('movies', (table) => {
table.dropColumn('photo_count');
});
};

View File

@ -0,0 +1,42 @@
const config = require('config');
exports.up = async (knex) => {
await knex.schema.alterTable('alerts', (table) => {
table.boolean('all')
.defaultTo(true);
});
await knex.schema.alterTable('alerts_entities', (table) => {
table.dropUnique('alert_id');
});
await knex.schema.createTable('alerts_matches', (table) => {
table.increments('id');
table.integer('alert_id')
.references('id')
.inTable('alerts')
.onDelete('cascade');
table.string('property');
table.string('expression');
});
await knex.raw(`
GRANT SELECT ON alerts_matches TO :visitor;
`, {
visitor: knex.raw(config.database.query.user),
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('alerts', (table) => {
table.dropColumn('all');
});
await knex.schema.alterTable('alerts_entities', (table) => {
table.unique('alert_id');
});
await knex.schema.dropTable('alerts_matches');
};

View File

@ -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;
`);

View File

@ -0,0 +1,11 @@
exports.up = async (knex) => {
await knex.schema.alterTable('banners', (table) => {
table.text('html');
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('banners', (table) => {
table.dropColumn('html');
});
};

3647
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "traxxx", "name": "traxxx",
"version": "1.225.10", "version": "1.233.0",
"description": "All the latest porn releases in one place", "description": "All the latest porn releases in one place",
"main": "src/app.js", "main": "src/app.js",
"scripts": { "scripts": {
@ -91,9 +91,11 @@
"convert": "^4.2.4", "convert": "^4.2.4",
"cookie": "^0.4.0", "cookie": "^0.4.0",
"csv-stringify": "^5.3.6", "csv-stringify": "^5.3.6",
"date-fns": "^2.30.0",
"dayjs": "^1.8.21", "dayjs": "^1.8.21",
"dompurify": "^2.0.11", "dompurify": "^2.0.11",
"ejs": "^3.0.1", "ejs": "^3.0.1",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-promise-router": "^4.1.0", "express-promise-router": "^4.1.0",
"express-react-views": "^0.11.0", "express-react-views": "^0.11.0",
@ -113,21 +115,23 @@
"knex": "^0.95.12", "knex": "^0.95.12",
"knex-migrate": "^1.7.4", "knex-migrate": "^1.7.4",
"longjohn": "^0.2.12", "longjohn": "^0.2.12",
"merge-anything": "^5.1.7",
"mime": "^2.4.4", "mime": "^2.4.4",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"moment": "^2.24.0",
"nanoid": "^3.1.30", "nanoid": "^3.1.30",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"object-merge-advanced": "^12.1.0", "object-merge-advanced": "^12.1.0",
"object.omit": "^3.0.0", "object.omit": "^3.0.0",
"opn": "^6.0.0",
"pg": "^8.5.1", "pg": "^8.5.1",
"postgraphile": "^4.13.0", "postgraphile": "^4.13.0",
"postgraphile-plugin-connection-filter": "^2.2.2", "postgraphile-plugin-connection-filter": "^2.2.2",
"promise-task-queue": "^1.2.0", "promise-task-queue": "^1.2.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"puppeteer": "^18.2.0", "puppeteer": "^20.5.0",
"puppeteer-extra": "^3.3.4", "puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.1", "puppeteer-extra-plugin-stealth": "^2.11.2",
"redis": "^4.6.7",
"sharp": "^0.29.2", "sharp": "^0.29.2",
"showdown": "^1.9.1", "showdown": "^1.9.1",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
@ -137,7 +141,7 @@
"tunnel": "0.0.6", "tunnel": "0.0.6",
"ua-parser-js": "^1.0.32", "ua-parser-js": "^1.0.32",
"undici": "^4.13.0", "undici": "^4.13.0",
"unprint": "^0.8.2", "unprint": "^0.10.11",
"url-pattern": "^1.0.3", "url-pattern": "^1.0.3",
"v-tooltip": "^2.0.3", "v-tooltip": "^2.0.3",
"video.js": "^7.11.4", "video.js": "^7.11.4",

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Some files were not shown because too many files have changed in this diff Show More