Compare commits
225 Commits
3bb96c3433
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a45fd152df | |||
| 60f01d859e | |||
| 6d4e033fb7 | |||
| bf635df863 | |||
| 1732b4cf4d | |||
| a2e268913a | |||
| dfb04e5e01 | |||
| c0ce844169 | |||
| 56acc42f17 | |||
| 7cee6639e7 | |||
| 62753a4af0 | |||
| 2d4d2b1105 | |||
| 09ed130327 | |||
| 1414a846ec | |||
| 19dc029e28 | |||
| 67176db933 | |||
| 95e8982696 | |||
| b8a03cd6fb | |||
| aad4ff8079 | |||
| 8821b3a00d | |||
| db43102487 | |||
| a3072a4967 | |||
| 983e24835f | |||
| 1c982124b0 | |||
| 6aaa3ad30c | |||
| 57dfa621df | |||
| e98254d444 | |||
| 56defe2c6f | |||
| a2f08c540c | |||
| 217decee06 | |||
| d93baf80ab | |||
| e409f3c214 | |||
| a1e080c20d | |||
| 6c8fce49d6 | |||
| 1a84f899e7 | |||
| ce107e6b65 | |||
| 515d3885c7 | |||
| 5194c5e156 | |||
| 5b53f53fd3 | |||
| 750b30d896 | |||
| b9afa61e01 | |||
| 490be8800a | |||
| 49ee6b4eee | |||
| ada81340ef | |||
| 5ae3b5d91c | |||
| bff3bc6a0b | |||
| 5496bced59 | |||
| 030d6dc835 | |||
| fc46ae00f8 | |||
| e22978cbe4 | |||
| 70049ed495 | |||
| 9e20af925f | |||
| 457afa5043 | |||
| c536a75f3d | |||
| a2d5828fda | |||
| 52d041c591 | |||
| 3bee1ac97d | |||
| 428d86b1ee | |||
| 5facacd066 | |||
| 0bf0b716b2 | |||
| 31c62e01f9 | |||
| a57b66cd95 | |||
| e4675e6e97 | |||
| bac0b768e2 | |||
| 74c69c698e | |||
| 87604ed848 | |||
| b6ca08727f | |||
| af99491533 | |||
| 461f6cf8fd | |||
| 2ad17c2279 | |||
| 9f3bc6e8de | |||
| 6dedf10846 | |||
| 6b6e31a1bb | |||
| f7016609a0 | |||
| fde2d607b8 | |||
| 54e9fd9f6a | |||
| 886f02c5fc | |||
| 8d57dfd2d2 | |||
| f9ba519dea | |||
| 2eb4678afc | |||
| 9558ce80b4 | |||
| 4569930a81 | |||
| 52b012402e | |||
| 82ff813225 | |||
| b7bd0fac03 | |||
| 9933b4fbf0 | |||
| c272e6c8b3 | |||
| 302f6a0621 | |||
| 23155520d2 | |||
| b788d78aab | |||
| cd9e4a5e8d | |||
| 8ec48ec43e | |||
| a27bc2c815 | |||
| 16f43066a4 | |||
| 6191e17c4e | |||
| 5ac7cfbc9a | |||
| bf802771de | |||
| 559dc21189 | |||
| 7fdb915921 | |||
| 19f0752b0f | |||
| 471ee42c0e | |||
| 1e089f731a | |||
| c026988a7b | |||
| 6281842a14 | |||
| 2380342328 | |||
| e28904b791 | |||
| ad7f1ce1fa | |||
| 327c7ab1db | |||
| 30303a80d3 | |||
| 66c1cbab6a | |||
| 34348890ec | |||
| edb4be379f | |||
| 363a6b4084 | |||
| fc5a0d209c | |||
| 63bee8f5e0 | |||
| 166f4ee7ce | |||
| 7702839b7a | |||
| cb1c884503 | |||
| 7bc9a90b81 | |||
| b2ca5a6713 | |||
| a4325f6ff6 | |||
| 03f57c1ef4 | |||
| 378e0edc75 | |||
| a2622aa536 | |||
| 602765bfbb | |||
| e32dd55220 | |||
| 4e181d5ff7 | |||
| ba971035a0 | |||
| c214ccf201 | |||
| 414499636e | |||
| d9bbd95fa1 | |||
| b63037ef74 | |||
| 37ccc3c3dd | |||
| a8bacaf083 | |||
| 0e23115cfe | |||
| e67ec5eca4 | |||
| 7a452db2f8 | |||
| 8b3e9d32d6 | |||
| d8d2ee6785 | |||
| 77b9acea32 | |||
| 68f15d4f74 | |||
| 42a83b2126 | |||
| a19c39d8eb | |||
| f06df01e70 | |||
| 0527674333 | |||
| 8d6da08519 | |||
| 2b338e32eb | |||
| aa8412863b | |||
| d4a486d2ae | |||
| ceac4ecc56 | |||
| 233829223e | |||
| 456b69f1ca | |||
| 83efdf59d4 | |||
| 02f2629f6b | |||
| 6796f7f258 | |||
| 84b9bbd1b6 | |||
| bc26e07812 | |||
| e05ca80f7c | |||
| 79eacee3f0 | |||
| 1fc77587ed | |||
| 77d37ce6b5 | |||
| 5dc829674a | |||
| 5ffc865c00 | |||
| ae085ad5ec | |||
| 05a93293fe | |||
| 117923ff1d | |||
| 47a748c623 | |||
| e9e0cf3600 | |||
| c530751a70 | |||
| f4acee53c4 | |||
| 14bca958fd | |||
| 0b5ce620d6 | |||
| 0435472489 | |||
| 253052f75a | |||
| 27acb09ced | |||
| 8f145e926e | |||
| 0dd4bcc7fe | |||
| b9c1c9914d | |||
| 90da4b592a | |||
| ac1e44f427 | |||
| 880d6369e4 | |||
| aad922ac30 | |||
| 9c99e464aa | |||
| ebc2895d7e | |||
| 45ed3be747 | |||
| 2afcdd6050 | |||
| b355ef4bf5 | |||
| 50280692e8 | |||
| 35699becf5 | |||
| 6237aa0c03 | |||
| b6398197ea | |||
| ea398a51aa | |||
| 87a800edc9 | |||
| 50d280a3c9 | |||
| 1de174a8c4 | |||
| c90d0c3f3c | |||
| 32202d8ab5 | |||
| 37b40f1744 | |||
| bd9a794e34 | |||
| 63ee4cae31 | |||
| ddaf5c3b42 | |||
| 7b78724bb4 | |||
| e4f410f293 | |||
| 55680b5150 | |||
| 1f7ad45393 | |||
| 243d28a030 | |||
| fecebee0c5 | |||
| cb47e3fd9e | |||
| 6492439f56 | |||
| 5ca478772c | |||
| dc876e7aa2 | |||
| 99cd551fc0 | |||
| c37a2ce1a1 | |||
| 0dabbc7233 | |||
| 538e6ede5b | |||
| fe1a9ed26b | |||
| 2121c51ae6 | |||
| f5d8c30ff3 | |||
| acef14b02c | |||
| 7dc1f78c80 | |||
| 73569704a5 | |||
| 1025285796 | |||
| 09bba4fe1e | |||
| c93a7189ce | |||
| 95008f9815 |
@@ -21,7 +21,7 @@
|
||||
"no-console": 0,
|
||||
"no-param-reassign": ["error", {
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": ["state", "acc"]
|
||||
"ignorePropertyModificationsFor": ["state", "acc", "req"]
|
||||
}],
|
||||
"vue/multi-word-component-names": 0,
|
||||
"vue/no-reserved-component-names": 0,
|
||||
@@ -32,7 +32,8 @@
|
||||
"vue/html-indent": ["error", "tab"],
|
||||
"vue/multiline-html-element-content-newline": 0,
|
||||
"vue/no-v-html": 0,
|
||||
"vue/singleline-html-element-content-newline": 0
|
||||
"vue/singleline-html-element-content-newline": 0,
|
||||
"vue/comment-directive": 0,
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@ config/*
|
||||
log/
|
||||
/media
|
||||
data/
|
||||
assets/*.mmdb
|
||||
assets/.geoipupdate.lock
|
||||
|
||||
52
assets/img/icons/matrix-full.svg
Normal file
52
assets/img/icons/matrix-full.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 75 32"
|
||||
id="svg1"
|
||||
sodipodi:docname="matrix-full.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="12.000629"
|
||||
inkscape:cx="27.79021"
|
||||
inkscape:cy="22.165505"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<title
|
||||
id="title1">Matrix (protocol) logo</title>
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
d="m0.936 0.732v30.52h2.194v0.732h-3.035v-31.98h3.034v0.732zm8.45 9.675v1.544h0.044a4.461 4.461 0 0 1 1.487-1.368c0.58-0.323 1.245-0.485 1.993-0.485 0.72 0 1.377 0.14 1.972 0.42 0.595 0.279 1.047 0.771 1.355 1.477 0.338-0.5 0.796-0.941 1.377-1.323 0.58-0.383 1.266-0.574 2.06-0.574 0.602 0 1.16 0.074 1.674 0.22 0.514 0.148 0.954 0.383 1.322 0.707 0.366 0.323 0.653 0.746 0.859 1.268 0.205 0.522 0.308 1.15 0.308 1.887v7.633h-3.127v-6.464c0-0.383-0.015-0.743-0.044-1.082a2.305 2.305 0 0 0-0.242-0.882 1.473 1.473 0 0 0-0.584-0.596c-0.257-0.146-0.606-0.22-1.047-0.22-0.44 0-0.796 0.085-1.068 0.253-0.272 0.17-0.485 0.39-0.639 0.662a2.654 2.654 0 0 0-0.308 0.927 7.074 7.074 0 0 0-0.078 1.048v6.354h-3.128v-6.398c0-0.338-7e-3 -0.673-0.021-1.004a2.825 2.825 0 0 0-0.188-0.916 1.411 1.411 0 0 0-0.55-0.673c-0.258-0.168-0.636-0.253-1.135-0.253a2.33 2.33 0 0 0-0.584 0.1 1.94 1.94 0 0 0-0.705 0.374c-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.357v6.619h-3.129v-11.41zm16.46 1.677a3.751 3.751 0 0 1 1.233-1.17 5.37 5.37 0 0 1 1.685-0.629 9.579 9.579 0 0 1 1.884-0.187c0.573 0 1.153 0.04 1.74 0.121 0.588 0.081 1.124 0.24 1.609 0.475 0.484 0.235 0.88 0.562 1.19 0.981 0.308 0.42 0.462 0.975 0.462 1.666v5.934c0 0.516 0.03 1.008 0.088 1.478 0.058 0.471 0.161 0.824 0.308 1.06h-3.171a4.435 4.435 0 0 1-0.22-1.104c-0.5 0.515-1.087 0.876-1.762 1.081a7.084 7.084 0 0 1-2.071 0.31c-0.544 0-1.05-0.067-1.52-0.2a3.472 3.472 0 0 1-1.234-0.617 2.87 2.87 0 0 1-0.826-1.059c-0.199-0.426-0.298-0.934-0.298-1.522 0-0.647 0.114-1.18 0.342-1.6 0.227-0.419 0.52-0.753 0.881-1.004 0.36-0.25 0.771-0.437 1.234-0.562 0.462-0.125 0.929-0.224 1.399-0.298 0.47-0.073 0.932-0.132 1.387-0.176 0.456-0.044 0.86-0.11 1.212-0.199 0.353-0.088 0.631-0.217 0.837-0.386s0.301-0.415 0.287-0.74c0-0.337-0.055-0.606-0.166-0.804a1.217 1.217 0 0 0-0.44-0.464 1.737 1.737 0 0 0-0.639-0.22 5.292 5.292 0 0 0-0.782-0.055c-0.617 0-1.101 0.132-1.454 0.397-0.352 0.264-0.558 0.706-0.617 1.323h-3.128c0.044-0.735 0.227-1.345 0.55-1.83zm6.179 4.423a5.095 5.095 0 0 1-0.639 0.165 9.68 9.68 0 0 1-0.716 0.11c-0.25 0.03-0.5 0.067-0.749 0.11a5.616 5.616 0 0 0-0.694 0.177 2.057 2.057 0 0 0-0.594 0.298c-0.17 0.125-0.305 0.284-0.408 0.474-0.103 0.192-0.154 0.434-0.154 0.728 0 0.28 0.051 0.515 0.154 0.706 0.103 0.192 0.242 0.342 0.419 0.453 0.176 0.11 0.381 0.187 0.617 0.231 0.234 0.044 0.477 0.066 0.726 0.066 0.617 0 1.094-0.102 1.432-0.309 0.338-0.205 0.587-0.452 0.75-0.739 0.16-0.286 0.26-0.576 0.297-0.87 0.036-0.295 0.055-0.53 0.055-0.707v-1.17a1.4 1.4 0 0 1-0.496 0.277zm11.86-6.1v2.096h-2.291v5.647c0 0.53 0.088 0.883 0.264 1.059 0.176 0.177 0.529 0.265 1.057 0.265 0.177 0 0.345-7e-3 0.507-0.022 0.161-0.015 0.316-0.037 0.463-0.066v2.426a7.49 7.49 0 0 1-0.882 0.089 21.67 21.67 0 0 1-0.947 0.022c-0.484 0-0.944-0.034-1.377-0.1a3.233 3.233 0 0 1-1.145-0.386 2.04 2.04 0 0 1-0.782-0.816c-0.191-0.353-0.287-0.816-0.287-1.39v-6.728h-1.894v-2.096h1.894v-3.42h3.129v3.42h2.29zm4.471 0v2.118h0.044a3.907 3.907 0 0 1 1.454-1.754 4.213 4.213 0 0 1 1.036-0.497 3.734 3.734 0 0 1 1.145-0.176c0.206 0 0.433 0.037 0.683 0.11v2.912a5.862 5.862 0 0 0-0.528-0.077 5.566 5.566 0 0 0-0.595-0.033c-0.573 0-1.058 0.096-1.454 0.287a2.52 2.52 0 0 0-0.958 0.783 3.143 3.143 0 0 0-0.518 1.158 6.32 6.32 0 0 0-0.154 1.434v5.14h-3.128v-11.4zm5.684-1.765v-2.582h3.128v2.582h-3.127zm3.128 1.765v11.4h-3.127v-11.4h3.128zm1.63 0h3.569l2.005 2.978 1.982-2.978h3.459l-3.745 5.339 4.208 6.067h-3.57l-2.378-3.596-2.38 3.596h-3.502l4.097-6.001zm15.3 20.84v-30.52h-2.194v-0.732h3.035v31.98h-3.035v-0.732z"
|
||||
id="path1" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Matrix (protocol) logo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
41
assets/img/icons/matrix.svg
Normal file
41
assets/img/icons/matrix.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
fill="#000000"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="matrix.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.23142407"
|
||||
inkscape:cx="412.66234"
|
||||
inkscape:cy="557.41825"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M0.844 0.735v30.531h2.197v0.735h-3.041v-32h3.041v0.735zM10.235 10.412v1.547h0.041c0.412-0.595 0.912-1.047 1.489-1.371 0.579-0.323 1.251-0.484 2-0.484 0.719 0 1.38 0.141 1.975 0.417 0.599 0.281 1.047 0.776 1.359 1.479 0.339-0.5 0.803-0.943 1.38-1.323 0.579-0.38 1.267-0.573 2.063-0.573 0.604 0 1.161 0.073 1.677 0.224 0.521 0.145 0.959 0.38 1.328 0.703 0.365 0.329 0.651 0.751 0.86 1.272 0.203 0.52 0.307 1.151 0.307 1.891v7.635h-3.129v-6.468c0-0.381-0.016-0.745-0.048-1.084-0.020-0.307-0.099-0.604-0.239-0.88-0.131-0.251-0.333-0.459-0.584-0.593-0.255-0.152-0.609-0.224-1.047-0.224-0.443 0-0.797 0.083-1.068 0.249-0.265 0.167-0.489 0.396-0.64 0.667-0.161 0.287-0.265 0.604-0.308 0.927-0.052 0.349-0.077 0.699-0.083 1.048v6.359h-3.131v-6.401c0-0.339-0.005-0.672-0.025-1-0.011-0.317-0.073-0.624-0.193-0.916-0.104-0.281-0.301-0.516-0.552-0.672-0.255-0.167-0.636-0.255-1.136-0.255-0.151 0-0.348 0.031-0.588 0.099-0.24 0.067-0.479 0.192-0.703 0.375-0.229 0.188-0.428 0.453-0.589 0.797-0.161 0.343-0.239 0.796-0.239 1.359v6.62h-3.131v-11.421zM31.156 31.265v-30.531h-2.197v-0.735h3.041v32h-3.041v-0.735z"
|
||||
id="path1"
|
||||
style="fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
4
assets/markdown.yaml
Normal file
4
assets/markdown.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
- wrap: ['[Scene details](', ')']
|
||||
items:
|
||||
- link
|
||||
- text: 'on [traxxx](https://traxxx.me/).'
|
||||
45
assets/mockup-release.ts
Normal file
45
assets/mockup-release.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
const now = new Date();
|
||||
|
||||
export default {
|
||||
id: 0,
|
||||
shootId: 12345,
|
||||
title: 'Nut For Human Consumption',
|
||||
slug: 'nut-for-human-consumption',
|
||||
link: 'https://traxxx.me/scene/0/nut-for-human-consumption',
|
||||
url: 'https://example.com/video/12345/nut-for-human-consumption',
|
||||
date: now,
|
||||
effectiveDate: now,
|
||||
createdAt: new Date(now.getFullYear(), 0, 1),
|
||||
actors: [
|
||||
{
|
||||
name: 'Chanel Chakra',
|
||||
gender: 'female',
|
||||
ageThen: 26,
|
||||
ageFromBirth: 31,
|
||||
dateOfBirth: new Date(1999, 2, 2),
|
||||
},
|
||||
{
|
||||
name: 'Mo The Fucker',
|
||||
gender: 'male',
|
||||
ageThen: 32,
|
||||
ageFromBirth: 37,
|
||||
dateOfBirth: new Date(1988, 5, 12),
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{ name: 'anal' },
|
||||
{ name: 'facefucking' },
|
||||
{ name: 'deepthroat' },
|
||||
{ name: 'blowjob' },
|
||||
{ name: 'facial' },
|
||||
],
|
||||
movies: [{
|
||||
title: `Best Of Traxxx ${String(now.getFullYear()).slice(2)}`,
|
||||
}],
|
||||
channel: {
|
||||
name: 'Traxxxed',
|
||||
},
|
||||
network: {
|
||||
name: 'Traxxx',
|
||||
},
|
||||
};
|
||||
125
assets/sfw.ejs
Normal file
125
assets/sfw.ejs
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/img/favicon/site.webmanifest">
|
||||
<link rel="mask-icon" href="/img/favicon/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="/img/favicon/favicon.ico">
|
||||
|
||||
<meta name="msapplication-TileColor" content="#b91d47">
|
||||
<meta name="msapplication-config" content="/img/favicon/browserconfig.xml">
|
||||
<meta name="theme-color" content="#f65596">
|
||||
|
||||
<meta property="og:title" content="traxxx" />
|
||||
<meta property="og:image" content="https://traxxx.me/img/og_logo.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
|
||||
|
||||
<title>traxxx - None shall pass</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-dark-10: #e54485;
|
||||
--primary: #f65596;
|
||||
--primary-light-10: #f075a6;
|
||||
--primary-light-20: #f2a6c4;
|
||||
--primary-light-30: #f7c9dc;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.explainer {
|
||||
margin-bottom: 3rem;
|
||||
font-size: 1.25rem;
|
||||
text-align: justify;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.useful {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
.links li {
|
||||
padding: .25rem 0;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.links a {
|
||||
padding: .5rem 1rem;
|
||||
border-radius: .5rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
background: var(--primary-dark-10);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="content">
|
||||
<h2 class="heading">Not so fast, rascal.</h2>
|
||||
|
||||
<p class="explainer">The content offered by traxxx is restricted in your jurisdiction.</p>
|
||||
|
||||
<% if (!noVpn) { %>
|
||||
<div class="useful">
|
||||
Useful links:
|
||||
<ul class="links">
|
||||
<li>
|
||||
<a
|
||||
href="https://mullvad.net/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>Mullvad VPN</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="https://protonvpn.com/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>Proton VPN</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
common
2
common
Submodule common updated: dc00c3d58a...ec4b15ce33
@@ -8,7 +8,7 @@
|
||||
type="search"
|
||||
placeholder="Search actors"
|
||||
class="input search"
|
||||
@keydown.enter="search"
|
||||
@search="search"
|
||||
>
|
||||
|
||||
<Icon
|
||||
@@ -281,7 +281,7 @@ function updateFilter(prop, value, reload = true) {
|
||||
.actors-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0 .25rem 2.25rem;
|
||||
padding: .5rem 0 .5rem 3rem;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
<li
|
||||
v-if="actor.residence"
|
||||
class="bio-item residence hideable"
|
||||
class="bio-item residence"
|
||||
:class="{ hideable: !!actor.origin }"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn>
|
||||
@@ -142,14 +142,24 @@
|
||||
class="bio-item figure"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="ruler" />Figure</dfn>
|
||||
<span class="bio-value">{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}</span>
|
||||
<span class="bio-value">
|
||||
{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
||||
<Icon
|
||||
v-if="actor.naturalBoobs === false || actor.naturalButt === false"
|
||||
v-tooltip="[actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', ')"
|
||||
icon="magic-wand2"
|
||||
class="enhanced"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="actor.naturalBoobs === false || actor.naturalButt === false"
|
||||
v-if="actor.naturalLips === false || actor.naturalLabia === false
|
||||
|| (actor.naturalBoobs === false && !actor.bust) || actor.boobsVolume || actor.boobsPlacement || actor.boobsIncision || actor.boobsImplant
|
||||
|| actor.buttVolume || actor.buttImplant"
|
||||
class="bio-item augmentations hideable"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="magic-wand2" />Enhanced</dfn>
|
||||
<dfn class="bio-label"><Icon icon="magic-wand2" />Enhancements</dfn>
|
||||
|
||||
<span class="bio-value">
|
||||
<div
|
||||
@@ -284,7 +294,7 @@
|
||||
|
||||
<li
|
||||
v-if="actor.agency"
|
||||
class="bio-item"
|
||||
class="bio-item hideable"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="user-tie" />Agency</dfn>
|
||||
|
||||
@@ -294,7 +304,10 @@
|
||||
>{{ actor.agency }}</span>
|
||||
</li>
|
||||
|
||||
<div class="bio-item bio-socials hideable">
|
||||
<div
|
||||
v-if="socials.length > 0"
|
||||
class="bio-item bio-socials hideable"
|
||||
>
|
||||
<ul class="socials">
|
||||
<a
|
||||
v-for="social in socials"
|
||||
@@ -436,6 +449,7 @@ const showExpand = [
|
||||
'bust',
|
||||
'cup',
|
||||
'eyes',
|
||||
'ethnicity',
|
||||
'hairColor',
|
||||
'hasPiercings',
|
||||
'hasTattoos',
|
||||
@@ -615,7 +629,9 @@ const socials = props.actor.socials.map((social) => ({
|
||||
}
|
||||
|
||||
.bio-value {
|
||||
display: inline-block;
|
||||
margin: 0 0 0 2rem;
|
||||
max-width: 20rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -838,7 +854,7 @@ const socials = props.actor.socials.map((social) => ({
|
||||
display: none;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: 10;
|
||||
bottom: -.25rem;
|
||||
}
|
||||
|
||||
@@ -944,6 +960,10 @@ const socials = props.actor.socials.map((social) => ({
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bio-value {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.expanded .bio-value {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="avatar-link no-link"
|
||||
>
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
v-if="actor.avatar && !restriction"
|
||||
:src="getPath(actor.avatar, 'thumbnail')"
|
||||
:style="{ 'background-image': `url(${getPath(actor.avatar, 'lazy')})` }"
|
||||
loading="lazy"
|
||||
@@ -103,7 +103,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user } = pageContext;
|
||||
const { user, restriction } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
:class="{ active: pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'actors' }"
|
||||
>Actor Revisions</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="/admin/entities"
|
||||
class="nav-link nolink"
|
||||
:class="{ active: pageContext.routeParams.section === 'entities' }"
|
||||
>Entity Health</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -446,9 +446,9 @@ async function createAlert() {
|
||||
allTags: tagAnd.value,
|
||||
allMatches: matchAnd.value,
|
||||
actors: actors.value.map((actor) => actor.id),
|
||||
tags: tags.value.map((tag) => tag.id),
|
||||
tagIds: tags.value.map((tag) => tag.id),
|
||||
matches: matches.value,
|
||||
entities: entities.value.map((entity) => entity.id),
|
||||
entityIds: entities.value.map((entity) => entity.id),
|
||||
notify: notify.value,
|
||||
email: email.value,
|
||||
stashes: stashes.value.map((stash) => stash.id),
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="restriction"
|
||||
class="restricted"
|
||||
>
|
||||
<div>Traxxx is restricted in your region</div>
|
||||
<a
|
||||
href="/sfw"
|
||||
class="link"
|
||||
>Learn more</a>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
v-if="campaign?.banner?.type === 'html'"
|
||||
v-else-if="campaign?.banner?.type === 'html'"
|
||||
ref="iframe"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
@@ -18,27 +29,40 @@
|
||||
:href="campaign.url || campaign.affiliate?.url"
|
||||
target="_blank"
|
||||
class="campaign"
|
||||
:style="{ 'background-image': backdrop && `url(${bannerSrc})` }"
|
||||
:class="{ backdrop }"
|
||||
data-umami-event="campaign-click"
|
||||
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
|
||||
>
|
||||
<img
|
||||
:src="bannerSrc"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
class="campaign-banner"
|
||||
>
|
||||
<div class="campaign-overlay">
|
||||
<img
|
||||
:src="bannerSrc"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
class="campaign-banner"
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { restriction } = pageContext;
|
||||
|
||||
const props = defineProps({
|
||||
campaign: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
backdrop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// console.log(props.campaign?.banner);
|
||||
// console.log(props.campaign);
|
||||
|
||||
const bannerSrc = (() => {
|
||||
if (props.campaign.banner) {
|
||||
@@ -60,6 +84,10 @@ const bannerSrc = (() => {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: .25rem;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.frame {
|
||||
@@ -67,11 +95,45 @@ const bannerSrc = (() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.campaign-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
.campaign-overlay {
|
||||
padding: 4px; /* margin for drop shadow */
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(125%);
|
||||
}
|
||||
|
||||
.dark .campaign-overlay {
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(75%);
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
filter: drop-shadow(0 0 2px var(--shadow-weak-20));
|
||||
}
|
||||
}
|
||||
|
||||
.restricted {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
font-weight: bold;
|
||||
padding: .5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
|
||||
<span class="avatar-meta">
|
||||
<span title="Dimensions">{{ avatar.width }}x{{ avatar.height }}</span>
|
||||
<span title="Sharpness">{{ avatar.sharpness.toFixed(2) }}</span>
|
||||
|
||||
<span
|
||||
v-if="avatar.sharpness"
|
||||
title="Sharpness"
|
||||
>{{ avatar.sharpness.toFixed(2) }}</span>
|
||||
</span>
|
||||
|
||||
<a
|
||||
|
||||
@@ -300,7 +300,7 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
}))];
|
||||
}
|
||||
|
||||
if (dateKeys.includes(key)) {
|
||||
if (dateKeys.includes(key) && value) {
|
||||
return [key, new Date(value)];
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (dateKeys.includes(delta.key)) {
|
||||
if (dateKeys.includes(delta.key) && delta.value) {
|
||||
return {
|
||||
...delta,
|
||||
value: new Date(delta.value),
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
>Some actors may not be listed, apply a filter or search to narrow down results.</div>
|
||||
|
||||
<div class="filters-sort">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="`Filter ${availableActors.length} actors`"
|
||||
class="input input-inline filters-search"
|
||||
>
|
||||
<label class="filter-search">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="`Filter ${availableActors.length} actors`"
|
||||
class="input input-inline filters-search"
|
||||
>
|
||||
|
||||
<Icon icon="search" />
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="filter-sort noselect"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<template>
|
||||
<div class="filter channels-container">
|
||||
<div class="filters-sort">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="`Filter ${channels.length} channels`"
|
||||
class="input input-inline filters-search"
|
||||
>
|
||||
<label class="filter-search">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="`Filter ${channels.length} channels`"
|
||||
class="input input-inline filters-search"
|
||||
>
|
||||
|
||||
<Icon icon="search" />
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-show="order === 'name'"
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="filteredCountries.length > 0"
|
||||
class="countries-container"
|
||||
>
|
||||
<input
|
||||
<label
|
||||
v-if="!filters.country"
|
||||
v-model="countryQuery"
|
||||
type="search"
|
||||
placeholder="Filter countries"
|
||||
class="input input-inline countries-search"
|
||||
class="filter-search"
|
||||
>
|
||||
<input
|
||||
v-model="countryQuery"
|
||||
type="search"
|
||||
placeholder="Filter countries"
|
||||
class="input input-inline countries-search filters-search"
|
||||
>
|
||||
|
||||
<div class="countries-list">
|
||||
<Icon icon="search" />
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="filteredCountries.length > 0"
|
||||
class="countries-list"
|
||||
>
|
||||
<Countries
|
||||
v-if="!countryQuery && !filters.country && topCountries.length < filteredCountries.length"
|
||||
:countries="topCountries"
|
||||
@@ -25,6 +33,13 @@
|
||||
@country="(alpha2) => emit('update', 'country', alpha2, true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="empty"
|
||||
>
|
||||
No matching countries
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,13 +75,12 @@ const filteredCountries = computed(() => allCountries.value.filter((country) =>
|
||||
<style scoped>
|
||||
.countries-container {
|
||||
border-bottom: solid 1px var(--shadow-weak-30);
|
||||
padding: .25rem 0;
|
||||
padding-bottom: .25rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.countries-search {
|
||||
width: 100%;
|
||||
margin-bottom: .25rem;
|
||||
.filter-search {
|
||||
|
||||
border-bottom: solid 1px var(--shadow-weak-40);
|
||||
}
|
||||
|
||||
@@ -75,6 +89,12 @@ const filteredCountries = computed(() => allCountries.value.filter((country) =>
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: .5rem;
|
||||
color: var(--glass);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:deep(.country.selected) .country-name {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
@@ -272,7 +272,6 @@ function toggleFilters(state) {
|
||||
&.order {
|
||||
padding: 0 .5rem 0 .25rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--glass);
|
||||
}
|
||||
@@ -286,6 +285,26 @@ function toggleFilters(state) {
|
||||
}
|
||||
}
|
||||
|
||||
.filter-search {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
padding: .5rem .25rem .5rem .75rem;
|
||||
fill: var(--glass-weak-20);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus + .icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-details {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@@ -366,15 +385,15 @@ function toggleFilters(state) {
|
||||
}
|
||||
|
||||
.filters-toggle {
|
||||
min-width: 2rem;
|
||||
height: 2.5rem;
|
||||
min-width: 2.75rem;
|
||||
height: 3rem;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 .25rem;
|
||||
position: absolute;
|
||||
top: .35rem;
|
||||
right: -2.5rem;
|
||||
right: -3.25rem;
|
||||
border-radius: 0 .5rem .5rem 0;
|
||||
background: var(--background);
|
||||
color: var(--glass);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<template>
|
||||
<div class="filter tags-container">
|
||||
<div class="filters-sort">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="`Filter ${groupedTags.available.length} tags`"
|
||||
class="input input-inline filters-search"
|
||||
>
|
||||
<label class="filter-search">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="`Filter ${groupedTags.available.length} tags`"
|
||||
class="input input-inline filters-search"
|
||||
>
|
||||
|
||||
<Icon icon="search" />
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-show="order === 'priority'"
|
||||
|
||||
50
components/filters/toggle.vue
Normal file
50
components/filters/toggle.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div
|
||||
class="filters-toggle open"
|
||||
@click.stop="emit('toggle')"
|
||||
><Icon icon="filter" /></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['toggle']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filters-toggle {
|
||||
min-width: 2.75rem;
|
||||
height: 3rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 .25rem;
|
||||
border-radius: 0 .5rem .5rem 0;
|
||||
background: var(--background);
|
||||
color: var(--glass);
|
||||
font-weight: bold;
|
||||
box-shadow: inset 0 0 3px var(--shadow-weak-30);
|
||||
|
||||
&.open {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
&.show-full {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.close .cross {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--glass);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,14 @@
|
||||
<footer class="footer">
|
||||
<span class="footer-segment">© traxxx</span>
|
||||
|
||||
<a
|
||||
v-if="env.links.matrix"
|
||||
:href="env.links.matrix"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="footer-segment footer-link nolink matrix"
|
||||
><Icon icon="matrix-full" /></a>
|
||||
|
||||
<a
|
||||
v-if="env.links.discord"
|
||||
:href="env.links.discord"
|
||||
@@ -56,7 +64,8 @@ const { env } = pageContext;
|
||||
}
|
||||
}
|
||||
|
||||
.discord .icon {
|
||||
.discord .icon,
|
||||
.matrix .icon {
|
||||
height: 1.25rem;
|
||||
width: 3.5rem;
|
||||
fill: var(--glass-strong-10);
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
</li>
|
||||
-->
|
||||
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
:class="{ active: activePage === 'scenes' }"
|
||||
href="/scenes"
|
||||
>Scenes</Link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="link"
|
||||
@@ -563,7 +571,7 @@ function blurSearch(event) {
|
||||
fill: var(--error);
|
||||
}
|
||||
|
||||
@media(--small) {
|
||||
@media(--compact) {
|
||||
.header-section {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,13 @@
|
||||
@play="playing = true; paused = false;"
|
||||
@pause="playing = false; paused = true;"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="(release.trailer || release.teaser).isRestricted"
|
||||
v-tooltip="'Restricted video'"
|
||||
icon="blocked"
|
||||
class="restricted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -71,9 +78,9 @@
|
||||
<div
|
||||
v-for="photo in [
|
||||
...(coversInAlbum ? release.covers : []),
|
||||
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
||||
...release.photos,
|
||||
...release.caps,
|
||||
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
||||
...(release.teaser?.mime.type === 'image' ? [release.poster] : []),
|
||||
]"
|
||||
:key="`photo-${photo.id}`"
|
||||
@@ -149,6 +156,10 @@ const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .banner {
|
||||
backdrop-filter: brightness(70%) blur(1rem);
|
||||
}
|
||||
|
||||
.poster-container {
|
||||
flex-shrink: 0;
|
||||
margin-right: .5rem;
|
||||
@@ -188,6 +199,15 @@ const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
||||
width: calc(21/9 * 16rem);
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 16/9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.restricted {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: .5rem;
|
||||
fill: var(--highlight-weak-10);
|
||||
}
|
||||
|
||||
:deep(.player) {
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
:actors="aggActors"
|
||||
@update="updateFilter"
|
||||
/>
|
||||
|
||||
<div class="filter">
|
||||
<Checkbox
|
||||
:checked="filters.requireCover"
|
||||
label="Require box cover"
|
||||
@change="(checked) => updateFilter('requireCover', checked, true)"
|
||||
/>
|
||||
</div>
|
||||
</Filters>
|
||||
|
||||
<div class="movies-container">
|
||||
@@ -100,6 +108,7 @@ import YearsFilter from '#/components/filters/years.vue';
|
||||
import ActorsFilter from '#/components/filters/actors.vue';
|
||||
import TagsFilter from '#/components/filters/tags.vue';
|
||||
import ChannelsFilter from '#/components/filters/channels.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Pagination from '#/components/pagination/pagination.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
@@ -139,6 +148,7 @@ const filters = ref({
|
||||
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
|
||||
entity: queryEntity,
|
||||
actors: queryActors,
|
||||
requireCover: !!urlParsed.search.cover,
|
||||
});
|
||||
|
||||
function getPath(targetScope, preserveQuery) {
|
||||
@@ -178,6 +188,7 @@ async function search(options = {}) {
|
||||
|
||||
const query = {
|
||||
q: filters.value.search || undefined,
|
||||
cover: filters.value.requireCover || undefined,
|
||||
};
|
||||
|
||||
const entity = filters.value.entity || pageEntity;
|
||||
@@ -241,7 +252,7 @@ function updateFilter(prop, value, reload = true) {
|
||||
.movies-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 1rem .25rem 3rem;
|
||||
padding: .5rem 1rem .25rem 4rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<Campaign
|
||||
v-if="campaigns?.pagination"
|
||||
:campaign="campaigns.pagination"
|
||||
class="campaign-pagination"
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -301,6 +302,10 @@ function getPath(page) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
:deep(.campaign-pagination) .campaign-banner {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media(--small-10) {
|
||||
.campaign {
|
||||
padding: 0;
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
v-tooltip="'Duration'"
|
||||
class="chapter-duration"
|
||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
||||
|
||||
<time
|
||||
v-if="chapter.date"
|
||||
v-tooltip="formatDate(chapter.date, 'yyyy-MM-dd hh:mm')"
|
||||
:datetime="chapter.date"
|
||||
class="chapter-date"
|
||||
>{{ formatDate(chapter.date, 'MMM d') }}</time>
|
||||
</span>
|
||||
|
||||
<div class="chapter-info">
|
||||
@@ -87,7 +94,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDuration } from '#/utils/format.js';
|
||||
import { formatDuration, formatDate } from '#/utils/format.js';
|
||||
|
||||
const props = defineProps({
|
||||
chapters: {
|
||||
@@ -96,8 +103,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
console.log(props.chapters);
|
||||
|
||||
const lastChapter = props.chapters.at(-1);
|
||||
const duration = lastChapter.time + lastChapter.duration;
|
||||
|
||||
@@ -140,9 +145,9 @@ const timeline = computed(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
padding: 0 .75rem;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
margin: 0 0 .75rem 0;
|
||||
color: var(--text-light);
|
||||
background: var(--grey-dark-40);
|
||||
font-size: .8rem;
|
||||
@@ -166,7 +171,7 @@ const timeline = computed(() => {
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
padding: 0 .5rem;
|
||||
padding: 0 .75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -183,6 +188,7 @@ const timeline = computed(() => {
|
||||
}
|
||||
|
||||
.chapter-description {
|
||||
text-align: justify;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
type="search"
|
||||
placeholder="Search scenes"
|
||||
class="search input"
|
||||
@keydown.enter="search"
|
||||
@search="search"
|
||||
>
|
||||
|
||||
<Icon
|
||||
@@ -65,6 +65,7 @@
|
||||
<Campaign
|
||||
v-if="campaigns?.meta"
|
||||
:campaign="campaigns.meta"
|
||||
class="campaign-meta"
|
||||
/>
|
||||
|
||||
<div class="views">
|
||||
@@ -155,6 +156,7 @@
|
||||
<Campaign
|
||||
v-if="campaigns?.scope"
|
||||
:campaign="campaigns.scope"
|
||||
class="campaign-scope"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
@@ -165,7 +167,10 @@
|
||||
v-if="item === 'campaign' && sceneCampaign"
|
||||
:key="`campaign-${item.id}`"
|
||||
>
|
||||
<Campaign :campaign="sceneCampaign" />
|
||||
<Campaign
|
||||
:campaign="sceneCampaign"
|
||||
:backdrop="true"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li
|
||||
@@ -393,7 +398,7 @@ function setView(newView) {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: .5rem 1rem .25rem 3rem;
|
||||
padding: .5rem 1rem .25rem 4rem;
|
||||
|
||||
.campaign {
|
||||
max-height: 6rem;
|
||||
@@ -431,11 +436,11 @@ function setView(newView) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
gap: .5rem;
|
||||
padding: .5rem 1rem 1rem 1rem;
|
||||
}
|
||||
|
||||
:deep(.campaign) .campaign-banner {
|
||||
border-radius: .25rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||
}
|
||||
:deep(.campaign-meta) .campaign-banner,
|
||||
:deep(.campaign-scope) .campaign-banner {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.scopes {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
class="input edit"
|
||||
@input="update"
|
||||
@blur="save(false)"
|
||||
@keydown.tab.prevent="indent"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
@@ -126,7 +127,7 @@ const changed = ref(false);
|
||||
const templateName = ref(initialTemplate?.name || `custom_${Date.now()}`);
|
||||
|
||||
function getSummary() {
|
||||
return processSummaryTemplate(template.value, props.release);
|
||||
return processSummaryTemplate(template.value, props.release, pageContext.env);
|
||||
}
|
||||
|
||||
const summary = ref(getSummary());
|
||||
@@ -227,6 +228,11 @@ function reset() {
|
||||
}
|
||||
}
|
||||
|
||||
function indent() {
|
||||
// YAML does not support tabs
|
||||
input.value.setRangeText(' ', input.value.selectionStart, input.value.selectionEnd, 'end');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
if (changed.value) {
|
||||
@@ -251,6 +257,7 @@ onMounted(() => {
|
||||
|
||||
.edit {
|
||||
flex-grow: 1;
|
||||
flex-basis: auto;
|
||||
min-height: 10rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<Meta
|
||||
:scene="scene"
|
||||
:user="user"
|
||||
class="meta-full"
|
||||
/>
|
||||
|
||||
@@ -129,7 +130,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
const { user } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
@@ -213,7 +214,7 @@ const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id ===
|
||||
.title {
|
||||
display: block;
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .4rem;
|
||||
margin-bottom: .3rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -238,7 +239,8 @@ const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id ===
|
||||
}
|
||||
|
||||
.actors {
|
||||
height: 1rem;
|
||||
height: 1.15rem;
|
||||
margin-bottom: .15rem;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -301,12 +303,12 @@ const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id ===
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0 .5rem .5rem .5rem;
|
||||
margin: 0 .5rem .45rem .5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: .6rem;
|
||||
margin-bottom: .6rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="serie-tile">
|
||||
<a
|
||||
v-if="serie.poster"
|
||||
:href="`/serie/${serie.id}/${serie.slug}`"
|
||||
class="poster-container"
|
||||
>
|
||||
<img
|
||||
v-if="serie.poster"
|
||||
:src="getPath(serie.poster, 'thumbnail')"
|
||||
:style="{ 'background-image': `url(${getPath(serie.poster, 'lazy')})` }"
|
||||
class="poster"
|
||||
|
||||
@@ -41,10 +41,12 @@ const cookies = Cookies.withConverter({
|
||||
const tags = {
|
||||
anal: 'anal',
|
||||
'anal-prolapse': 'anal prolapse',
|
||||
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||
pissing: 'pissing',
|
||||
gay: 'gay',
|
||||
transsexual: 'transsexual',
|
||||
bisexual: 'bisexual',
|
||||
compilation: 'compilation',
|
||||
bts: 'behind the scenes',
|
||||
vr: 'virtual reality',
|
||||
};
|
||||
@@ -65,9 +67,10 @@ function toggleTag(tag, isChecked) {
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
padding: 1rem;
|
||||
width: 30rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
<ul class="nolist menu">
|
||||
<li
|
||||
class="menu-item"
|
||||
:class="{ active: activePage === 'updates' }"
|
||||
:class="{ active: activePage === 'scenes' }"
|
||||
>
|
||||
<a
|
||||
href="/updates"
|
||||
href="/scenes"
|
||||
class="menu-link nolink"
|
||||
>Updates</a>
|
||||
>Scenes</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="stash">
|
||||
<div class="stash-header">
|
||||
<a
|
||||
:href="`/stash/${profile.username}/${stash.slug}`"
|
||||
:href="`/stash/${profile.username}/${stash.slug}/${primaryDomain}`"
|
||||
class="stash-name ellipsis nolink"
|
||||
>
|
||||
<span class="ellipsis">{{ stash.name }}</span>
|
||||
@@ -151,6 +151,14 @@ const stashNameInput = ref(null);
|
||||
const showRenameDialog = ref(false);
|
||||
const done = ref(true);
|
||||
|
||||
const domainCounts = {
|
||||
scenes: props.stash.stashedScenes,
|
||||
actors: props.stash.stashedActors,
|
||||
movies: props.stash.stashedMovies,
|
||||
};
|
||||
|
||||
const primaryDomain = Object.entries(domainCounts).toSorted((domainA, domainB) => domainB[1] - domainA[1])[0][0];
|
||||
|
||||
async function setPublic(isPublic) {
|
||||
if (done.value === false) {
|
||||
return;
|
||||
|
||||
64
components/tags/logo.vue
Normal file
64
components/tags/logo.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<a
|
||||
v-if="photo?.entity"
|
||||
v-tooltip="photo.entity.name"
|
||||
:to="`/${photo.entity.type}/${photo.entity.slug}`"
|
||||
>
|
||||
<img
|
||||
v-if="favicon && photo.entity.type !== 'network' && !photo.entity.independent && photo.entity.parent"
|
||||
:src="`/logos/${photo.entity.parent.slug}/favicon.png`"
|
||||
class="album-logo favicon"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="favicon"
|
||||
:src="`/logos/${photo.entity.slug}/favicon.png`"
|
||||
class="album-logo favicon"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="photo.entity.type !== 'network' && !photo.entity.independent && photo.entity.parent"
|
||||
:src="`/logos/${photo.entity.parent.slug}/${photo.entity.slug}.png`"
|
||||
class="album-logo"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="`/logos/${photo.entity.slug}/network.png`"
|
||||
class="album-logo"
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
photo: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
favicon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.album-logo {
|
||||
max-height: .75rem;
|
||||
max-width: 5rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: .5rem;
|
||||
transition: transform .25s ease, opacity .25s ease;
|
||||
filter: drop-shadow(0 0 2px var(--shadow-weak));
|
||||
}
|
||||
|
||||
@media(--small) {
|
||||
.album-logo:not(.favicon) {
|
||||
max-height: .5rem;
|
||||
max-width: 3.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
150
components/tags/photos.vue
Normal file
150
components/tags/photos.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="photos nobar">
|
||||
<Campaign
|
||||
v-if="campaigns?.photos"
|
||||
:campaign="campaigns.photos"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="photo in photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
:title="photo.comment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="photo-container"
|
||||
>
|
||||
<img
|
||||
:src="getPath(photo, 'thumbnail', { local: true })"
|
||||
:style="{ 'background-image': `url(${getPath(photo, 'lazy', { local: true })})` }"
|
||||
:alt="photo.comment"
|
||||
:width="photo.width"
|
||||
:height="photo.height"
|
||||
class="photo"
|
||||
loading="lazy"
|
||||
@load="emit('load', $event)"
|
||||
>
|
||||
|
||||
<Logo :photo="photo" />
|
||||
|
||||
<a
|
||||
v-if="photo.comment && photo.entity"
|
||||
:href="`/${photo.entity.type}/${photo.entity.slug}`"
|
||||
class="photo-comment"
|
||||
>{{ photo.comment }} for {{ photo.entity.name }}</a>
|
||||
|
||||
<span
|
||||
v-else-if="photo.comment"
|
||||
class="photo-comment"
|
||||
>{{ photo.comment }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
import Logo from '#/components/tags/logo.vue';
|
||||
import Campaign from '#/components/campaigns/campaign.vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['load', 'campaign']);
|
||||
|
||||
const { campaigns } = inject('pageContext');
|
||||
|
||||
const photos = computed(() => {
|
||||
/* sfw not currently implemented
|
||||
if (props.tag.poster && this.$store.state.ui.sfw) {
|
||||
return [props.tag.poster].concat(props.tag.photos).map((photo) => photo.sfw);
|
||||
}
|
||||
|
||||
if (this.$store.state.ui.sfw) {
|
||||
return props.tag.photos.map((photo) => photo.sfw);
|
||||
}
|
||||
*/
|
||||
|
||||
if (props.tag.poster) {
|
||||
return [props.tag.poster].concat(props.tag.photos);
|
||||
}
|
||||
|
||||
return props.tag.photos;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.photos {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 0.5rem;
|
||||
padding: .5rem;
|
||||
border-bottom: solid 1px var(--shadow-weak-40);
|
||||
background: var(--background-base-10);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.photo-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
font-size: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
.photo-comment {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
::v-deep(.album-logo) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photo,
|
||||
.campaign {
|
||||
height: 14rem;
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.photo {
|
||||
object-fit: cover;
|
||||
object-position: 50% 0;
|
||||
border-radius: .25rem;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
box-shadow: 0 0px 3px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.photo-comment {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
color: var(--text-light);
|
||||
background: var(--shadow);
|
||||
font-size: .9rem;
|
||||
text-shadow: 0 0 3px var(--shadow);
|
||||
text-decoration: none;
|
||||
white-space: normal;
|
||||
line-height: 1.25;
|
||||
transform: translateY(100%);
|
||||
transition: transform .25s ease;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
}
|
||||
|
||||
@media(--small) {
|
||||
.photo,
|
||||
.campaign {
|
||||
height: 11rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,27 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="manager">
|
||||
<div class="keys-header">
|
||||
<h2 class="heading">API keys</h2>
|
||||
<section class="profile-section">
|
||||
<div class="section-header">
|
||||
<h3 class="heading">API Keys</h3>
|
||||
|
||||
<div class="keys-actions">
|
||||
<Icon
|
||||
v-tooltip="'Flush all keys'"
|
||||
icon="stack-cancel"
|
||||
@click="flushKeys"
|
||||
/>
|
||||
<div class="keys-actions">
|
||||
<Icon
|
||||
v-tooltip="'Flush all keys'"
|
||||
icon="stack-cancel"
|
||||
class="keys-flush"
|
||||
@click="flushKeys"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="createKey"
|
||||
>New key</button>
|
||||
</div>
|
||||
<button
|
||||
class="button"
|
||||
@click="createKey"
|
||||
>
|
||||
<Icon icon="key" />
|
||||
<span class="button-label">New key</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manager">
|
||||
<div
|
||||
v-if="newKey"
|
||||
class="newkey"
|
||||
@@ -48,6 +52,7 @@
|
||||
<span class="key-actions">
|
||||
<Icon
|
||||
icon="bin"
|
||||
class="key-remove"
|
||||
@click="removeKey(key)"
|
||||
/>
|
||||
</span>
|
||||
@@ -86,12 +91,12 @@
|
||||
<h3 class="info-heading">HTTP headers</h3>
|
||||
|
||||
<code class="headers">
|
||||
API-User: {{ user.id }}<br>
|
||||
API-Key: YourSecurelyStoredApiKey12345678
|
||||
API-User: <strong>{{ user.id }}</strong><br>
|
||||
API-Key: <em>YourSecretKey</em>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -108,7 +113,7 @@ const keys = ref(pageContext.pageProps.keys);
|
||||
const newKey = ref(null);
|
||||
|
||||
async function createKey() {
|
||||
const key = await post('/keys', null, {
|
||||
const key = await post('/me/keys', null, {
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
@@ -148,19 +153,6 @@ function copyKey(event) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.manager {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.keys-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -170,10 +162,9 @@ function copyKey(event) {
|
||||
|
||||
.keys-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
> .icon {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
}
|
||||
@@ -185,12 +176,18 @@ function copyKey(event) {
|
||||
fill: var(--glass);
|
||||
|
||||
&:hover {
|
||||
fill: var(--error);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keys-flush,
|
||||
.key-remove {
|
||||
&:hover {
|
||||
fill: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
.keys {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
|
||||
@@ -201,12 +198,14 @@ function copyKey(event) {
|
||||
.key {
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
border-radius: .25rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.key-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -219,16 +218,17 @@ function copyKey(event) {
|
||||
.icon {
|
||||
width: .9rem;
|
||||
height: .9rem;
|
||||
fill: var(--glass-strong-10);
|
||||
margin-right: .25rem;
|
||||
fill: var(--glass);
|
||||
}
|
||||
}
|
||||
|
||||
.key-header .key-value {
|
||||
padding: .5rem .5rem .25rem .5rem;
|
||||
padding: .5rem .75rem;
|
||||
}
|
||||
|
||||
.key-details .key-value {
|
||||
padding: .25rem .5rem .5rem .5rem;
|
||||
padding: .25rem .75rem .75rem .75rem;
|
||||
}
|
||||
|
||||
.key-identifier {
|
||||
@@ -238,7 +238,9 @@ function copyKey(event) {
|
||||
}
|
||||
|
||||
.key-actions .icon {
|
||||
padding: 0 .5rem .5rem .5rem;
|
||||
height: 1rem;
|
||||
padding: .75rem .75rem .5rem .75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.newkey {
|
||||
@@ -285,7 +287,17 @@ function copyKey(event) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media(--compact) {
|
||||
.manager {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(--small-20) {
|
||||
.manager {
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
.keys {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
title: 'traxxx',
|
||||
origin: 'http://localhost:5100', // only used for absolute links
|
||||
database: {
|
||||
owner: {
|
||||
host: '127.0.0.1',
|
||||
@@ -57,34 +58,105 @@ module.exports = {
|
||||
address: 'http://localhost:3000/script.js',
|
||||
siteId: '1b28ac3b-d229-43bf-aec9-75cf0a72a466',
|
||||
},
|
||||
restrictions: {
|
||||
enabled: false,
|
||||
modes: [
|
||||
null, // easier for 0 to mean disabled
|
||||
'block', // 1
|
||||
'censor', // 2
|
||||
],
|
||||
regions: {
|
||||
// Europe
|
||||
DE: 1, // Germany
|
||||
FR: 1, // France
|
||||
GB: 1, // Great Britain / United Kingdom
|
||||
IT: 1, // Italy
|
||||
// Asia & Oceania
|
||||
AU: 1, // Australia
|
||||
CN: 1, // China
|
||||
// Americas
|
||||
US: {
|
||||
AL: 1, // Alabama
|
||||
AR: 1, // Arkansas
|
||||
AZ: 1, // Arizona
|
||||
FL: 1, // Florida
|
||||
GA: 1, // Georgia
|
||||
ID: 1, // Idaho
|
||||
IN: 1, // Indiana
|
||||
KS: 1, // Kansas
|
||||
KY: 1, // Kentucky
|
||||
LA: 1, // Louisiana
|
||||
MO: 1, // Missouri
|
||||
MS: 1, // Mississippi
|
||||
MT: 1, // Montana
|
||||
NC: 1, // North Carolina
|
||||
ND: 1, // North Dakota
|
||||
NE: 1, // Nebraska
|
||||
OH: 1, // Ohio
|
||||
OK: 1, // Oklahoma
|
||||
SC: 1, // South Carolina
|
||||
SD: 1, // South Dakota
|
||||
TN: 1, // Tennessee
|
||||
TX: 1, // Texas
|
||||
UT: 1, // Utah
|
||||
VA: 1, // Virginia
|
||||
WY: 1, // Wyoming
|
||||
}, // only Florida
|
||||
},
|
||||
noVpn: [
|
||||
'AE', // United Arab Emirates
|
||||
'BY', // Belarus
|
||||
'CN', // China
|
||||
'IQ', // Iraq
|
||||
'IR', // Iran
|
||||
'KP', // North Korea
|
||||
'OM', // Oman
|
||||
'RU', // Russia
|
||||
'TM', // Turkmenistan
|
||||
'TR', // Turkey
|
||||
],
|
||||
censors: [ // additional to default filter
|
||||
'ball',
|
||||
'bisexual',
|
||||
'blow',
|
||||
'blowbang',
|
||||
'condom',
|
||||
'cowgirl',
|
||||
'creampie',
|
||||
'doggy',
|
||||
'facial',
|
||||
'finger',
|
||||
'gay',
|
||||
'hole',
|
||||
'horny',
|
||||
'lesbian',
|
||||
'masturbation',
|
||||
'milf',
|
||||
'missionary',
|
||||
'prolapse',
|
||||
'nymph',
|
||||
'sex',
|
||||
'swallowing',
|
||||
'squirt',
|
||||
'sucking',
|
||||
'threesome',
|
||||
'trans',
|
||||
],
|
||||
},
|
||||
auth: {
|
||||
login: true,
|
||||
signup: true,
|
||||
usernameLength: [2, 24],
|
||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
||||
captcha: {
|
||||
enabled: false,
|
||||
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
||||
secretKey: '0x0000000000000000000000000000000000000000',
|
||||
},
|
||||
},
|
||||
bans: {
|
||||
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
||||
},
|
||||
socials: {
|
||||
urls: {
|
||||
cashapp: 'https://cash.app/${handle}', // eslint-disable-line no-template-curly-in-string
|
||||
fansly: 'https://fansly.com/{handle}',
|
||||
instagram: 'https://www.instagram.com/{handle}',
|
||||
linktree: 'https://linktr.ee/{handle}',
|
||||
loyalfans: 'https://www.loyalfans.com/{handle}',
|
||||
manyvids: 'https://{handle}.manyvids.com',
|
||||
onlyfans: 'https://onlyfans.com/{handle}',
|
||||
pornhub: 'https://www.pornhub.com/model/{handle}',
|
||||
reddit: 'https://www.reddit.com/u/{handle}',
|
||||
twitter: 'https://x.com/{handle}',
|
||||
},
|
||||
prefix: {
|
||||
default: '@',
|
||||
cashapp: '$',
|
||||
reddit: 'u/',
|
||||
},
|
||||
},
|
||||
apiAccess: {
|
||||
graphqlEnabled: true,
|
||||
keySize: 24, // bytes
|
||||
@@ -102,6 +174,7 @@ module.exports = {
|
||||
links: {
|
||||
content: 'mailto:content@traxxx.me',
|
||||
discord: 'https://discord.gg/gY6fnq6jJV',
|
||||
matrix: 'https://matrix.to/#/#traxxx:matrix.unknown.name',
|
||||
},
|
||||
stashes: {
|
||||
nameLength: [2, 24],
|
||||
@@ -114,8 +187,9 @@ module.exports = {
|
||||
},
|
||||
media: {
|
||||
path: './media',
|
||||
assetPath: '/img',
|
||||
assetPath: '',
|
||||
mediaPath: '/media',
|
||||
s3Path: 'https://s3.wasabisys.com',
|
||||
videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,9 @@ module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'traxxx',
|
||||
script: 'npm',
|
||||
args: 'run server:prod',
|
||||
// script: 'npm',
|
||||
// args: 'run server:prod',
|
||||
script: './src/app.js',
|
||||
exec_mode: 'cluster',
|
||||
instances: 2,
|
||||
restart_delay: 3000,
|
||||
|
||||
266
package-lock.json
generated
266
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "traxxx-web",
|
||||
"version": "0.39.9",
|
||||
"version": "0.46.16",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.39.9",
|
||||
"version": "0.46.16",
|
||||
"dependencies": {
|
||||
"@brillout/json-serializer": "^0.5.8",
|
||||
"@dicebear/collection": "^7.0.5",
|
||||
@@ -13,6 +13,8 @@
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@maxmind/geoip2-node": "^6.3.4",
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"@toycode/markdown-it-class": "^1.2.4",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
@@ -28,6 +30,7 @@
|
||||
"cron": "^3.1.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^3.0.0",
|
||||
"ejs": "^4.0.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -37,6 +40,8 @@
|
||||
"floating-vue": "^5.2.2",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-parse-resolve-info": "^4.13.0",
|
||||
"graphql-scalars": "^1.24.2",
|
||||
"hcaptcha": "^0.2.0",
|
||||
"ip-cidr": "^4.0.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"knex": "^3.1.0",
|
||||
@@ -49,6 +54,7 @@
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"object.omit": "^3.0.0",
|
||||
"obscenity": "^0.4.6",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pg": "^8.11.3",
|
||||
"redis": "^4.6.12",
|
||||
@@ -3022,6 +3028,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@hcaptcha/vue3-hcaptcha": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz",
|
||||
"integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue": "^3.2.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
@@ -3209,6 +3227,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@maxmind/geoip2-node": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.3.4.tgz",
|
||||
"integrity": "sha512-BTRFHCX7Uie4wVSPXsWQfg0EVl4eGZgLCts0BTKAP+Eiyt1zmF2UPyuUZkaj0R59XSDYO+84o1THAtaenUoQYg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"maxmind": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@modyfi/vite-plugin-yaml": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
||||
@@ -4512,9 +4539,10 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -5562,6 +5590,21 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
|
||||
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.9.1"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.18"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.616",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
||||
@@ -6775,6 +6818,36 @@
|
||||
"moment": "^2.29.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -7226,6 +7299,21 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/graphql-scalars": {
|
||||
"version": "1.24.2",
|
||||
"resolved": "https://registry.npmjs.org/graphql-scalars/-/graphql-scalars-1.24.2.tgz",
|
||||
"integrity": "sha512-FoZ11yxIauEnH0E5rCUkhDXHVn/A6BBfovJdimRZCQlFCl+h7aVvarKmI15zG4VtQunmCDdqdtNs6ixThy3uAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
|
||||
@@ -7303,6 +7391,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hcaptcha": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz",
|
||||
"integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||
@@ -8009,6 +8103,23 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
@@ -8474,6 +8585,20 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/maxmind": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
|
||||
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mmdb-lib": "3.0.2",
|
||||
"tiny-lru": "11.4.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -8617,6 +8742,16 @@
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"node_modules/mmdb-lib": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
|
||||
"integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
@@ -8955,6 +9090,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obscenity": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.6.tgz",
|
||||
"integrity": "sha512-pHk7kNN7j3L3zGhhGnwxjvXIGsPpLrcZl2r58fqWh/V/rH6b/dafscj2sMmAY+A/9/wPsocLmimgGk2DKeKsFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -10698,6 +10842,15 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"node_modules/tiny-lru": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
|
||||
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
||||
@@ -13745,6 +13898,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@hcaptcha/vue3-hcaptcha": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz",
|
||||
"integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==",
|
||||
"requires": {
|
||||
"vue": "^3.2.19"
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
@@ -13876,6 +14037,14 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"@maxmind/geoip2-node": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.3.4.tgz",
|
||||
"integrity": "sha512-BTRFHCX7Uie4wVSPXsWQfg0EVl4eGZgLCts0BTKAP+Eiyt1zmF2UPyuUZkaj0R59XSDYO+84o1THAtaenUoQYg==",
|
||||
"requires": {
|
||||
"maxmind": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"@modyfi/vite-plugin-yaml": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
||||
@@ -14715,9 +14884,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -15474,6 +15643,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"ejs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
|
||||
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
|
||||
"requires": {
|
||||
"jake": "^10.9.1"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.616",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
||||
@@ -16390,6 +16567,32 @@
|
||||
"moment": "^2.29.1"
|
||||
}
|
||||
},
|
||||
"filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"requires": {
|
||||
"minimatch": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -16700,6 +16903,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphql-scalars": {
|
||||
"version": "1.24.2",
|
||||
"resolved": "https://registry.npmjs.org/graphql-scalars/-/graphql-scalars-1.24.2.tgz",
|
||||
"integrity": "sha512-FoZ11yxIauEnH0E5rCUkhDXHVn/A6BBfovJdimRZCQlFCl+h7aVvarKmI15zG4VtQunmCDdqdtNs6ixThy3uAg==",
|
||||
"requires": {
|
||||
"tslib": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"has-bigints": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
|
||||
@@ -16747,6 +16958,11 @@
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"hcaptcha": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz",
|
||||
"integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw=="
|
||||
},
|
||||
"html-encoding-sniffer": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||
@@ -17220,6 +17436,16 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"jake": {
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"requires": {
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"picocolors": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
@@ -17580,6 +17806,15 @@
|
||||
"typed-function": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"maxmind": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
|
||||
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
|
||||
"requires": {
|
||||
"mmdb-lib": "3.0.2",
|
||||
"tiny-lru": "11.4.7"
|
||||
}
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -17681,6 +17916,11 @@
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"mmdb-lib": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
|
||||
"integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
@@ -17932,6 +18172,11 @@
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"obscenity": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.6.tgz",
|
||||
"integrity": "sha512-pHk7kNN7j3L3zGhhGnwxjvXIGsPpLrcZl2r58fqWh/V/rH6b/dafscj2sMmAY+A/9/wPsocLmimgGk2DKeKsFQ=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -19170,6 +19415,11 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"tiny-lru": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
|
||||
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "npm run server:dev",
|
||||
"dev": "node ./src/app",
|
||||
"prod": "npm run build && npm run server:prod",
|
||||
"build": "vite build",
|
||||
"server:dev": "node ./src/app",
|
||||
@@ -13,6 +13,8 @@
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@maxmind/geoip2-node": "^6.3.4",
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"@toycode/markdown-it-class": "^1.2.4",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
@@ -28,6 +30,7 @@
|
||||
"cron": "^3.1.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^3.0.0",
|
||||
"ejs": "^4.0.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -37,6 +40,8 @@
|
||||
"floating-vue": "^5.2.2",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-parse-resolve-info": "^4.13.0",
|
||||
"graphql-scalars": "^1.24.2",
|
||||
"hcaptcha": "^0.2.0",
|
||||
"ip-cidr": "^4.0.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"knex": "^3.1.0",
|
||||
@@ -49,6 +54,7 @@
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"object.omit": "^3.0.0",
|
||||
"obscenity": "^0.4.6",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pg": "^8.11.3",
|
||||
"redis": "^4.6.12",
|
||||
@@ -86,7 +92,7 @@
|
||||
"overrides": {
|
||||
"vite": "$vite"
|
||||
},
|
||||
"version": "0.39.9",
|
||||
"version": "0.46.16",
|
||||
"imports": {
|
||||
"#/*": "./*.js"
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
|
||||
<div
|
||||
class="photos nobar"
|
||||
:class="{ 'has-avatar': actor.avatar, 'has-photos': actor.avatar ? actor.photos.length > 1 : actor.photos.length > 0 }"
|
||||
:class="{ 'has-avatar': actor.avatar, 'has-photos': actor.avatar ? photos.length > 1 : photos.length > 0 }"
|
||||
>
|
||||
<div
|
||||
v-for="photo in actor.photos"
|
||||
v-for="photo in photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
class="photo-container"
|
||||
:class="{ avatar: photo.isAvatar }"
|
||||
@@ -92,6 +92,9 @@ const { pageProps, routeParams } = pageContext;
|
||||
const { actor } = pageProps;
|
||||
|
||||
const domain = routeParams.domain;
|
||||
|
||||
const badCredits = ['Pierre Woodman']; // consistently horrible photos
|
||||
const photos = actor.photos.filter((photo) => photo.entropy > 5.5 && !badCredits.includes(photo.credit));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fetchMovies } from '#/src/movies.js';
|
||||
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||
import { curateMoviesQuery } from '#/src/web/movies.js';
|
||||
import { fetchCountries } from '#/src/countries.js';
|
||||
import { getRandomCampaigns, getCampaignIndex } from '#/src/campaigns.js';
|
||||
|
||||
async function fetchReleases(pageContext) {
|
||||
if (pageContext.routeParams.domain === 'movies') {
|
||||
@@ -40,16 +41,27 @@ export async function onBeforeRender(pageContext) {
|
||||
throw redirect(`/login?r=${encodeURIComponent(pageContext.urlOriginal)}`);
|
||||
}
|
||||
|
||||
const [[actor], actorReleases, countries] = await Promise.all([
|
||||
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
|
||||
fetchReleases(pageContext),
|
||||
isEditing && fetchCountries(),
|
||||
]);
|
||||
const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user);
|
||||
|
||||
if (!actor) {
|
||||
throw render(404, `Cannot find actor '${pageContext.routeParams.actorId}'.`);
|
||||
}
|
||||
|
||||
const [actorReleases, campaigns, countries] = await Promise.all([
|
||||
fetchReleases(pageContext),
|
||||
getRandomCampaigns([
|
||||
// don't show meta campaign, too intrusive under actor bio
|
||||
{ minRatio: 3 },
|
||||
pageContext.routeParams.domain === 'scenes'
|
||||
? { minRatio: 0.75, maxRatio: 1.25 }
|
||||
: null,
|
||||
].filter(Boolean), { tagFilter: pageContext.tagFilter }),
|
||||
isEditing && fetchCountries(),
|
||||
]);
|
||||
|
||||
const campaignIndex = getCampaignIndex(actorReleases.limit);
|
||||
const [paginationCampaign, sceneCampaign] = campaigns;
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: isEditing
|
||||
@@ -60,6 +72,11 @@ export async function onBeforeRender(pageContext) {
|
||||
countries,
|
||||
...actorReleases,
|
||||
},
|
||||
campaigns: {
|
||||
index: campaignIndex,
|
||||
scenes: actorReleases.limit > 5 && sceneCampaign,
|
||||
pagination: paginationCampaign,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,16 @@
|
||||
class="row"
|
||||
>
|
||||
<div class="item-header">
|
||||
<div class="key">{{ item.label || item.key }}</div>
|
||||
<div class="key">
|
||||
{{ item.label || item.key }}
|
||||
|
||||
<Icon
|
||||
v-if="item.note"
|
||||
v-tooltip="item.note"
|
||||
icon="info2"
|
||||
class="item-note"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item-actions noselect">
|
||||
<Icon
|
||||
@@ -443,6 +452,7 @@ const fields = computed(() => [
|
||||
{
|
||||
key: 'augmentation',
|
||||
type: 'augmentation',
|
||||
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
||||
value: {
|
||||
naturalBoobs: actor.value.naturalBoobs,
|
||||
boobsVolume: actor.value.boobsVolume,
|
||||
@@ -503,6 +513,7 @@ const fields = computed(() => [
|
||||
{
|
||||
key: 'piercings',
|
||||
type: 'has',
|
||||
note: 'Excludes earrings',
|
||||
value: {
|
||||
has: actor.value.hasPiercings,
|
||||
description: actor.value.piercings,
|
||||
@@ -685,10 +696,22 @@ async function submit() {
|
||||
|
||||
.key {
|
||||
width: 10rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-note{
|
||||
fill: var(--glass);
|
||||
padding: .5rem .75rem;
|
||||
cursor: help;
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
class="link"
|
||||
>Actor Revisions</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/admin/entities"
|
||||
class="link"
|
||||
>Entity Health</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Admin>
|
||||
</template>
|
||||
|
||||
187
pages/admin/entities/+Page.vue
Normal file
187
pages/admin/entities/+Page.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<Admin class="page">
|
||||
<div class="header">
|
||||
<div class="params">
|
||||
<label>
|
||||
Alert: <input
|
||||
v-model="alertThreshold"
|
||||
type="number"
|
||||
placeholder="Alert threshold"
|
||||
class="input"
|
||||
> weeks
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Dead: <input
|
||||
v-model="deadThreshold"
|
||||
type="number"
|
||||
placeholder="Alert threshold"
|
||||
class="input"
|
||||
> months
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="attention">{{ alertEntities.length }} entities might require your attention</span>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-header">Entity</th>
|
||||
<th class="table-header">Network</th>
|
||||
|
||||
<th
|
||||
class="table-header noselect"
|
||||
@click="sort('releases')"
|
||||
>Releases</th>
|
||||
|
||||
<th
|
||||
class="table-header noselect"
|
||||
@click="sort('latest')"
|
||||
>Latest release</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="entity in alertEntities"
|
||||
:key="`entity-${entity.id}`"
|
||||
>
|
||||
<td
|
||||
:title="entity.id"
|
||||
class="table-cell table-name ellipsis"
|
||||
>
|
||||
<a
|
||||
:href="`/${entity.type}/${entity.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ entity.name }}</a>
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="entity.parent"
|
||||
:title="entity.paren?.id"
|
||||
class="table-cell table-name ellipsis"
|
||||
>
|
||||
<a
|
||||
:href="`/network/${entity.parent.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ entity.parent.name }}</a>
|
||||
</td>
|
||||
|
||||
<td v-else />
|
||||
|
||||
<td class="table-cell table-total">{{ entity.totalReleases }}</td>
|
||||
|
||||
<td
|
||||
class="table-cell table-date"
|
||||
:class="{ alert: entity.latestReleaseDate && entity.latestReleaseDate < alertDate }"
|
||||
>{{ entity.latestReleaseDate && format(entity.latestReleaseDate, 'yyyy-MM-dd hh:mm') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Admin>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
inject,
|
||||
} from 'vue';
|
||||
|
||||
import { format, subMonths, subWeeks } from 'date-fns';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
import Admin from '#/components/admin/admin.vue';
|
||||
|
||||
const {
|
||||
pageProps,
|
||||
urlParsed,
|
||||
meta,
|
||||
} = inject('pageContext');
|
||||
|
||||
const { entities } = pageProps;
|
||||
|
||||
const alertThreshold = ref(Number(urlParsed.search.alert) || 12);
|
||||
const deadThreshold = ref(Number(urlParsed.search.dead) || 36);
|
||||
const order = urlParsed.search.order || 'desc';
|
||||
|
||||
const alertDate = computed(() => subWeeks(meta.now, alertThreshold.value));
|
||||
const deadDate = computed(() => subMonths(meta.now, deadThreshold.value));
|
||||
|
||||
const alertEntities = computed(() => entities.filter((entity) => entity.latestReleaseDate > deadDate.value && entity.latestReleaseDate < alertDate.value));
|
||||
|
||||
function sort(sorting) {
|
||||
navigate('/admin/entities', {
|
||||
sort: sorting,
|
||||
order: order === 'desc' ? 'asc' : 'desc',
|
||||
alert: alertThreshold.value,
|
||||
dead: deadThreshold.value,
|
||||
}, {
|
||||
redirect: true,
|
||||
});
|
||||
}
|
||||
|
||||
watch([alertThreshold, deadThreshold], () => {
|
||||
navigate('/admin/entities', {
|
||||
...urlParsed.search,
|
||||
alert: alertThreshold.value,
|
||||
dead: deadThreshold.value,
|
||||
}, {
|
||||
redirect: false,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
|
||||
.input {
|
||||
width: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attention {
|
||||
margin-left: 2rem;
|
||||
color: var(--warn);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-name {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.table-total {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
color: var(--warn);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
27
pages/admin/entities/+onBeforeRender.js
Normal file
27
pages/admin/entities/+onBeforeRender.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
import { fetchEntityHealths } from '#/src/entities.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
if (!pageContext.user || pageContext.user.role === 'user') {
|
||||
throw render(404);
|
||||
}
|
||||
|
||||
const {
|
||||
entities,
|
||||
} = await fetchEntityHealths({
|
||||
sort: pageContext.urlParsed.search.sort || 'releases',
|
||||
order: pageContext.urlParsed.search.order || 'desc',
|
||||
}, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: pageContext.routeParams.section,
|
||||
pageProps: {
|
||||
entities,
|
||||
},
|
||||
routeParams: {
|
||||
section: 'entities',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { fetchUserKeys } from '#/src/auth.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const keys = await fetchUserKeys(pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: 'API keys',
|
||||
pageProps: {
|
||||
keys,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -70,7 +70,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button button-submit">Log in</button>
|
||||
<button
|
||||
class="button button-submit"
|
||||
:disabled="submitted"
|
||||
>Log in</button>
|
||||
|
||||
<a
|
||||
v-if="allowSignup"
|
||||
@@ -94,11 +97,13 @@ const allowSignup = pageContext.env.allowSignup;
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
const submitted = ref(false);
|
||||
const errorMsg = ref(null);
|
||||
const userInput = ref(null);
|
||||
const showPassword = ref(false);
|
||||
|
||||
async function login() {
|
||||
submitted.value = true;
|
||||
errorMsg.value = null;
|
||||
|
||||
try {
|
||||
@@ -111,6 +116,8 @@ async function login() {
|
||||
navigate(pageContext.urlParsed.search.r ? decodeURIComponent(pageContext.urlParsed.search.r) : `/user/${loginUser.username}`, null, { redirect: true });
|
||||
} catch (error) {
|
||||
errorMsg.value = error.message;
|
||||
} finally {
|
||||
submitted.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button button-submit">Sign up</button>
|
||||
<VueHCaptcha
|
||||
v-if="env.captcha.enabled"
|
||||
:sitekey="env.captcha.siteKey"
|
||||
class="captcha"
|
||||
@verify="(verification) => captcha = verification"
|
||||
@expired="captcha = null"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="button button-submit"
|
||||
:disabled="submitted"
|
||||
>Sign up</button>
|
||||
|
||||
<a
|
||||
href="/login"
|
||||
@@ -124,12 +135,13 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject } from 'vue';
|
||||
import VueHCaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
const { user, env } = pageContext;
|
||||
|
||||
const username = ref('');
|
||||
const email = ref('');
|
||||
@@ -137,28 +149,39 @@ const password = ref('');
|
||||
const passwordConfirm = ref('');
|
||||
|
||||
const errorMsg = ref(null);
|
||||
const submitted = ref(false);
|
||||
const userInput = ref(null);
|
||||
const showPassword = ref(false);
|
||||
const captcha = ref(null);
|
||||
|
||||
async function signup() {
|
||||
errorMsg.value = null;
|
||||
submitted.value = true;
|
||||
|
||||
if (password.value !== passwordConfirm.value) {
|
||||
errorMsg.value = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
if (env.captcha.enabled && !captcha.value) {
|
||||
errorMsg.value = 'Please complete the CAPTCHA';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newUser = await post('/users', {
|
||||
username: username.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
redirect: pageContext.urlParsed.search.r,
|
||||
captcha: captcha.value,
|
||||
});
|
||||
|
||||
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
||||
} catch (error) {
|
||||
errorMsg.value = error.message;
|
||||
} finally {
|
||||
submitted.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +252,16 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.captcha {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.button-submit {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--error);
|
||||
color: var(--text-light);
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
@@ -75,20 +75,19 @@ const networksBySlug = Object.fromEntries(networks.map((network) => [network.slu
|
||||
const popularNetworks = [
|
||||
'21sextury',
|
||||
'adulttime',
|
||||
'amateurallure',
|
||||
'analvids',
|
||||
'bamvisions',
|
||||
'bang',
|
||||
'bangbros',
|
||||
'blowpass',
|
||||
'brazzers',
|
||||
'burningangel',
|
||||
'digitalplayground',
|
||||
'dogfartnetwork',
|
||||
'dorcel',
|
||||
'elegantangel',
|
||||
'evilangel',
|
||||
'fakehub',
|
||||
'hentaied',
|
||||
'hookuphotshot',
|
||||
'hussiepass',
|
||||
'julesjordan',
|
||||
@@ -102,7 +101,10 @@ const popularNetworks = [
|
||||
'pornworld',
|
||||
'private',
|
||||
'realitykings',
|
||||
'rickysroom',
|
||||
'score',
|
||||
'teamskeet',
|
||||
'kellymadison',
|
||||
'vixen',
|
||||
'xempire',
|
||||
].map((slug) => networksBySlug[slug]).filter(Boolean);
|
||||
@@ -120,15 +122,11 @@ const sections = [
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
// const tags = Object.values(Object.fromEntries(networks.flatMap((entity) => entity.tags).map((tag) => [tag.id, tag])));
|
||||
|
||||
async function search() {
|
||||
navigate('/channels', { q: query.value || undefined }, { redirect: true });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('load', (event) => {
|
||||
console.log(event);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,7 +3,9 @@ import { fetchEntities } from '#/src/entities.js';
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const networks = await fetchEntities(pageContext.urlParsed.search.q
|
||||
? { query: pageContext.urlParsed.search.q }
|
||||
: { type: 'primary' });
|
||||
: { type: 'primary' }, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
>{{ entity.name }}</h2>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="user?.abilities.some((ability) => ability.plainUrls)"
|
||||
:href="entity.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="plainurl"
|
||||
><Icon icon="link" /></a>
|
||||
|
||||
<Heart
|
||||
domain="entities"
|
||||
:item="entity"
|
||||
@@ -154,7 +162,7 @@ import Movies from '#/components/movies/movies.vue';
|
||||
import Domains from '#/components/domains/domains.vue';
|
||||
import Heart from '#/components/stashes/heart.vue';
|
||||
|
||||
const { pageProps, routeParams } = inject('pageContext');
|
||||
const { pageProps, routeParams, user } = inject('pageContext');
|
||||
const { entity } = pageProps;
|
||||
|
||||
const children = ref(null);
|
||||
@@ -163,22 +171,7 @@ const expanded = ref(false);
|
||||
const scrollable = computed(() => children.value?.scrollWidth > children.value?.clientWidth);
|
||||
const domain = routeParams.domain;
|
||||
|
||||
const entityUrl = (() => {
|
||||
if (!entity.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entity.affiliate?.parameters) {
|
||||
return entity.url;
|
||||
}
|
||||
|
||||
const newParams = new URLSearchParams({
|
||||
...Object.fromEntries(new URL(entity.url).searchParams),
|
||||
...Object.fromEntries(new URLSearchParams(entity.affiliate.parameters)),
|
||||
});
|
||||
|
||||
return `${entity.url}?${newParams}`;
|
||||
})();
|
||||
const entityUrl = entity.affiliateUrl || entity.url || null;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -326,6 +319,25 @@ const entityUrl = (() => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plainurl {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .25rem;
|
||||
margin-top: -.25rem;
|
||||
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: auto;
|
||||
padding: .5rem;
|
||||
fill: var(--highlight);
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media(--small-20) {
|
||||
.logo {
|
||||
padding: .5rem 1rem;
|
||||
|
||||
@@ -20,7 +20,9 @@ async function fetchReleases(pageContext, entityId) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchScenes(await curateScenesQuery({
|
||||
@@ -32,7 +34,9 @@ async function fetchReleases(pageContext, entityId) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
@@ -47,19 +51,21 @@ export async function onBeforeRender(pageContext) {
|
||||
[entity],
|
||||
entityReleases,
|
||||
] = await Promise.all([
|
||||
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user),
|
||||
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
fetchReleases(pageContext, entityId),
|
||||
]);
|
||||
|
||||
const campaigns = await getRandomCampaigns([
|
||||
{
|
||||
entityIds: [entity.id, entity.parent?.id].filter(Boolean),
|
||||
minRatio: 1.5,
|
||||
minRatio: 3,
|
||||
allowRandomFallback: false,
|
||||
},
|
||||
{
|
||||
entityIds: [entity.id, entity.parent?.id].filter(Boolean),
|
||||
minRatio: 1.5,
|
||||
minRatio: 3,
|
||||
allowRandomFallback: false,
|
||||
},
|
||||
pageContext.routeParams.domain === 'scenes' ? {
|
||||
|
||||
@@ -9,7 +9,9 @@ export async function onBeforeRender(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 50,
|
||||
dedupe: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
|
||||
@@ -277,12 +277,15 @@ const scenes = pageContext.pageProps.scenes;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 .5rem 1rem 0;
|
||||
margin: 0 .5rem 0 0;
|
||||
line-height: 1.25;
|
||||
|
||||
/*
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
*/
|
||||
}
|
||||
|
||||
.notitle {
|
||||
|
||||
@@ -18,7 +18,7 @@ function getTitle(movie) {
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [[movie], movieScenes] = await Promise.all([
|
||||
fetchMoviesById([Number(pageContext.routeParams.movieId)], pageContext.user),
|
||||
fetchMoviesById([Number(pageContext.routeParams.movieId)], pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchScenes(await curateScenesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: 'oldest',
|
||||
@@ -27,7 +27,9 @@ export async function onBeforeRender(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!movie) {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
|
||||
<Link
|
||||
:href="scene.watchUrl"
|
||||
:href="user?.abilities?.some((ability) => ability.plainUrls) ? scene.url : scene.watchUrl"
|
||||
:title="scene.date ? formatDate(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${formatDate(scene.createdAt, 'y-MM-dd')}`"
|
||||
target="_blank"
|
||||
class="date nolink"
|
||||
@@ -249,12 +249,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chapters
|
||||
v-if="scene.chapters.length > 0"
|
||||
:chapters="scene.chapters"
|
||||
class="section"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="scene.description"
|
||||
class="section"
|
||||
@@ -264,6 +258,18 @@
|
||||
<p class="description">{{ scene.description }}</p>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="scene.chapters.length > 0"
|
||||
class="section"
|
||||
>
|
||||
<h3 class="heading">Chapters</h3>
|
||||
|
||||
<Chapters
|
||||
:chapters="scene.chapters"
|
||||
class="section"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="campaigns?.scene"
|
||||
class="section"
|
||||
@@ -328,7 +334,7 @@
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-if="user && assets.templates.length > 0"
|
||||
v-if="templates.length > 0"
|
||||
class="nolist templates"
|
||||
>
|
||||
<Icon icon="markup" />
|
||||
@@ -343,6 +349,43 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="user && scene.fingerprints.length > 0"
|
||||
class="section fingerprints"
|
||||
>
|
||||
<h3 class="heading">Fingerprints</h3>
|
||||
|
||||
<div class="fingerprints-container">
|
||||
<table class="fingerprints-table">
|
||||
<thead class="fingerprints-head">
|
||||
<tr class="fingerprints-header">
|
||||
<th class="fingerprints-heading">Hash</th>
|
||||
<th class="fingerprints-heading">Type</th>
|
||||
<th class="fingerprints-heading">Duration</th>
|
||||
<th class="fingerprints-heading">Submissions</th>
|
||||
<th class="fingerprints-heading">Source</th>
|
||||
<th class="fingerprints-heading">First added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="fingerprints-body">
|
||||
<tr
|
||||
v-for="fingerprint in scene.fingerprints"
|
||||
:key="`fingerprint-${fingerprint.hash}`"
|
||||
class="fingerprint"
|
||||
>
|
||||
<td class="fingerprint-field fingerprint-hash">{{ fingerprint.hash }}</td>
|
||||
<td class="fingerprint-field fingerprint-type">{{ fingerprint.type.toUpperCase() }}</td>
|
||||
<td class="fingerprint-field fingerprint-duration">{{ formatDuration(fingerprint.duration) }}</td>
|
||||
<td class="fingerprint-field fingerprint-submission">{{ fingerprint.submissions }}</td>
|
||||
<td class="fingerprint-field fingerprint-source">{{ fingerprint.source || 'traxxx' }}</td>
|
||||
<td class="fingerprint-field fingerprint-date">{{ formatDate(fingerprint.createdAt, 'yyyy-MM-dd') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="scene-actions section"
|
||||
>
|
||||
@@ -381,6 +424,7 @@ import Heart from '#/components/stashes/heart.vue';
|
||||
import Campaign from '#/components/campaigns/campaign.vue';
|
||||
|
||||
import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved
|
||||
import markdownTemplate from '#/assets/markdown.yaml?raw'; // eslint-disable-line import/no-unresolved
|
||||
|
||||
const cookies = Cookies.withConverter({
|
||||
write: (value) => value,
|
||||
@@ -399,6 +443,8 @@ const { scene } = pageProps;
|
||||
const showSummaryDialog = ref(false);
|
||||
|
||||
const qualities = {
|
||||
4320: '8K',
|
||||
2280: '5K',
|
||||
2160: '4K',
|
||||
1440: 'Quad HD',
|
||||
1080: 'Full HD',
|
||||
@@ -418,6 +464,11 @@ const templates = [
|
||||
name: 'traxxx',
|
||||
template: defaultTemplate,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'markdown',
|
||||
template: markdownTemplate,
|
||||
},
|
||||
...(assets?.templates || []),
|
||||
];
|
||||
|
||||
@@ -431,7 +482,7 @@ function selectTemplate(templateId, allowFallback = true) {
|
||||
|
||||
const template = targetTemplate || templates[0];
|
||||
|
||||
summary.value = processSummaryTemplate(template.template, scene);
|
||||
summary.value = processSummaryTemplate(template.template, scene, env);
|
||||
selectedTemplate.value = template.id;
|
||||
|
||||
cookies.set('selectedTemplate', String(templateId));
|
||||
@@ -538,6 +589,8 @@ function copySummary() {
|
||||
.title {
|
||||
margin: .25rem .5rem .5rem 0;
|
||||
line-height: 1.25;
|
||||
|
||||
/*
|
||||
display: -webkit-box;
|
||||
|
||||
&:not(:active) {
|
||||
@@ -545,6 +598,7 @@ function copySummary() {
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
.notitle {
|
||||
@@ -729,6 +783,55 @@ function copySummary() {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fingerprints {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.fingerprints-container {
|
||||
max-height: 10rem;
|
||||
overflow-y: auto;
|
||||
resize: vertical;
|
||||
|
||||
&[style*="height"] {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.fingerprints-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fingerprints-head {
|
||||
background: var(--background-base-10);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.fingerprints-heading {
|
||||
color: var(--glass);
|
||||
font-weight: normal;
|
||||
padding: .25rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fingerprint {
|
||||
&:nth-child(2n + 1) {
|
||||
background: var(--glass-weak-50);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--glass-weak-40);
|
||||
}
|
||||
}
|
||||
|
||||
.fingerprint-field {
|
||||
padding: .25rem;
|
||||
}
|
||||
|
||||
.fingerprint-hash {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
@media(--compact) {
|
||||
.content {
|
||||
margin: 0;
|
||||
@@ -763,7 +866,7 @@ function copySummary() {
|
||||
}
|
||||
|
||||
.entity-logo {
|
||||
width: 7.5rem;
|
||||
max-width: 7.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export async function onBeforeRender(pageContext) {
|
||||
includeAssets: true,
|
||||
includePartOf: true,
|
||||
actorStashes: true,
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
const [campaigns, tagIds] = await Promise.all([
|
||||
|
||||
@@ -66,7 +66,16 @@
|
||||
class="row"
|
||||
>
|
||||
<div class="item-header">
|
||||
<div class="key">{{ item.label || item.key }}</div>
|
||||
<div class="key">
|
||||
{{ item.label || item.key }}
|
||||
|
||||
<Icon
|
||||
v-if="item.note"
|
||||
v-tooltip="item.note"
|
||||
icon="info2"
|
||||
class="item-note"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item-actions">
|
||||
<Icon
|
||||
@@ -262,11 +271,13 @@ const fields = computed(() => [
|
||||
key: 'title',
|
||||
type: 'string',
|
||||
value: scene.value.title,
|
||||
note: 'Do not correct language errors unless source was updated.',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
type: 'text',
|
||||
value: scene.value.description,
|
||||
note: 'Do not correct language errors unless source was updated.',
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
@@ -282,6 +293,7 @@ const fields = computed(() => [
|
||||
key: 'duration',
|
||||
type: 'duration',
|
||||
value: [Math.floor(scene.value.duration / 3600), Math.floor((scene.value.duration % 3600) / 60), scene.value.duration % 60],
|
||||
simplify: false,
|
||||
},
|
||||
{
|
||||
key: 'productionDate',
|
||||
@@ -304,16 +316,16 @@ const fields = computed(() => [
|
||||
}]),
|
||||
]);
|
||||
|
||||
function simplifyArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => item.hash || item.id);
|
||||
function simplifyArray(field) {
|
||||
if (Array.isArray(field.value) && field.simplify !== false) {
|
||||
return field.value.map((item) => item.hash || item.id);
|
||||
}
|
||||
|
||||
return value;
|
||||
return field.value;
|
||||
}
|
||||
|
||||
const editing = ref(new Set());
|
||||
const edits = ref(Object.fromEntries(fields.value.map((field) => [field.key, simplifyArray(field.value)])));
|
||||
const edits = ref(Object.fromEntries(fields.value.map((field) => [field.key, simplifyArray(field)])));
|
||||
const comment = ref(null);
|
||||
const apply = ref(user.role !== 'user');
|
||||
const submitted = ref(false);
|
||||
@@ -332,7 +344,6 @@ const keyMap = {
|
||||
function toggleField(item) {
|
||||
if (editing.value.has(item.key)) {
|
||||
editing.value.delete(item.key);
|
||||
// delete edits.value[item.key];
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -358,7 +369,8 @@ async function submit() {
|
||||
|
||||
return [[key, edits.value[key]]];
|
||||
})),
|
||||
duration: edits.value.duration
|
||||
// duration: edits.value.duration
|
||||
duration: editing.value.has('duration') && edits.value.duration
|
||||
? (((edits.value.duration[0] || 0) * 3600) + ((edits.value.duration[1] || 0) * 60) + (edits.value.duration[2] || 0)) || null
|
||||
: undefined,
|
||||
},
|
||||
@@ -407,6 +419,8 @@ async function submit() {
|
||||
|
||||
.key {
|
||||
width: 8rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -472,6 +486,16 @@ async function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
.item-note{
|
||||
fill: var(--glass);
|
||||
padding: .5rem .75rem;
|
||||
cursor: help;
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
24
pages/scenes/+Page.vue
Normal file
24
pages/scenes/+Page.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="updates">
|
||||
<Scenes
|
||||
:show-filters="true"
|
||||
:show-meta="true"
|
||||
:show-scope-tabs="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// import { inject } from 'vue';
|
||||
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
|
||||
// const pageContext = inject('pageContext');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.updates {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
49
pages/scenes/+onBeforeRender.js
Normal file
49
pages/scenes/+onBeforeRender.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { fetchScenes } from '#/src/scenes.js';
|
||||
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||
import { getRandomCampaigns, getCampaignIndex } from '#/src/campaigns.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [
|
||||
sceneResults,
|
||||
campaigns,
|
||||
] = await Promise.all([
|
||||
fetchScenes(await curateScenesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: pageContext.routeParams.scope || 'latest',
|
||||
isShowcased: null,
|
||||
tagFilter: pageContext.tagFilter,
|
||||
}), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
||||
aggregate: true,
|
||||
dedupe: true,
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
getRandomCampaigns([
|
||||
{ minRatio: 0.75, maxRatio: 1.25 },
|
||||
{ minRatio: 1.5 },
|
||||
], { tagFilter: pageContext.tagFilter }),
|
||||
]);
|
||||
|
||||
const {
|
||||
scenes,
|
||||
} = sceneResults;
|
||||
|
||||
const campaignIndex = getCampaignIndex(scenes.length);
|
||||
const [sceneCampaign, paginationCampaign] = campaigns;
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: pageContext.routeParams.scope,
|
||||
pageProps: {
|
||||
...sceneResults,
|
||||
},
|
||||
campaigns: {
|
||||
index: campaignIndex,
|
||||
scenes: scenes.length > 5 && sceneCampaign,
|
||||
pagination: paginationCampaign,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
21
pages/scenes/+route.js
Normal file
21
pages/scenes/+route.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { match } from 'path-to-regexp';
|
||||
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||
|
||||
const path = '/scenes/:scope?/:page?';
|
||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
const matched = urlMatch(pageContext.urlPathname);
|
||||
|
||||
if (matched) {
|
||||
return {
|
||||
routeParams: {
|
||||
scope: matched.params.scope || 'latest',
|
||||
page: matched.params.page || '1',
|
||||
path,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -83,7 +83,7 @@
|
||||
Found {{ sceneTotal }} {{ sceneTotal > 1 ? 'scenes' : 'scene' }}
|
||||
|
||||
<a
|
||||
:href="`/updates/results/?q=${query}`"
|
||||
:href="`/scenes/results/?q=${query}`"
|
||||
class="link"
|
||||
>Full scene results</a>
|
||||
</span>
|
||||
|
||||
@@ -16,22 +16,24 @@ export async function onBeforeRender(pageContext) {
|
||||
}), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 15,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchActors(curateActorsQuery(pageContext.urlQuery), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 10,
|
||||
order: ['results', 'desc'],
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchMovies(await curateMoviesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: pageContext.routeParams.scope || 'results',
|
||||
}), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 5,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchEntities({
|
||||
query: pageContext.urlParsed.search.q,
|
||||
limit: 5,
|
||||
}, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -103,9 +103,22 @@
|
||||
</div>
|
||||
|
||||
<time
|
||||
:datetime="serie.date.toISOString()"
|
||||
class="date ellipsis"
|
||||
>{{ formatDate(serie.date, 'MMMM d, y') }}</time>
|
||||
:datetime="serie.effectiveDate.toISOString()"
|
||||
class="date ellipsis compact-hide"
|
||||
:class="{ nodate: !serie.date }"
|
||||
>{{ formatDate(serie.effectiveDate, {
|
||||
month: 'MMMM y',
|
||||
year: 'y',
|
||||
}[serie.datePrecision] || 'MMMM d, y') }}</time>
|
||||
|
||||
<time
|
||||
:datetime="serie.effectiveDate.toISOString()"
|
||||
class="date ellipsis compact-show"
|
||||
:class="{ nodate: !serie.date }"
|
||||
>{{ formatDate(serie.effectiveDate, {
|
||||
month: 'MMM y',
|
||||
year: 'y',
|
||||
}[serie.datePrecision] || 'MMM d, y') }}</time>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
@@ -408,6 +421,11 @@ const scenes = pageContext.pageProps.scenes;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nodate {
|
||||
color: var(--highlight);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.info,
|
||||
.header {
|
||||
border-top: none;
|
||||
@@ -537,6 +555,10 @@ const scenes = pageContext.pageProps.scenes;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.compact-show {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(--small) {
|
||||
.content {
|
||||
margin: 0;
|
||||
@@ -621,5 +643,13 @@ const scenes = pageContext.pageProps.scenes;
|
||||
.actors {
|
||||
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
||||
}
|
||||
|
||||
.compact-show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.compact-hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
>
|
||||
<img
|
||||
v-if="tag.poster"
|
||||
:src="`/${tag.poster.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/${tag.poster.lazy})` }"
|
||||
:src="getPath(tag.poster, 'thumbnail', { local: true })"
|
||||
:style="{ 'background-image': `url(${getPath(tag.poster, 'lazy', { local: true })})` }"
|
||||
:title="tag.poster.comment"
|
||||
class="thumb"
|
||||
loading="lazy"
|
||||
@@ -89,18 +89,10 @@
|
||||
>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="tag.poster?.entity"
|
||||
:href="`/${tag.poster.entity.type}/${tag.poster.entity.slug}`"
|
||||
class="favicon-link"
|
||||
>
|
||||
<img
|
||||
:src="!tag.poster.entity.parent || tag.poster.entity.isIndependent ? `/logos/${tag.poster.entity.slug}/favicon.png` : `/logos/${tag.poster.entity.parent.slug}/favicon.png`"
|
||||
:alt="tag.poster.entity.name"
|
||||
:title="tag.poster.entity.name"
|
||||
class="favicon"
|
||||
>
|
||||
</a>
|
||||
<Logo
|
||||
:photo="tag.poster"
|
||||
:favicon="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
@@ -119,6 +111,9 @@ import { ref, onMounted, inject } from 'vue';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
import events from '#/src/events.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import Logo from '#/components/tags/logo.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const showcase = pageContext.pageProps.tagShowcase;
|
||||
@@ -151,11 +146,13 @@ function calculateActiveCategory() {
|
||||
|
||||
activeCategory.value = newCategory;
|
||||
|
||||
/* this understandably causes jittering in Firefox, why the need to scroll into the category we're already in?
|
||||
const activeLink = document.querySelector(`a[href="#${activeCategory.value}"]`);
|
||||
|
||||
activeLink.scrollIntoView({
|
||||
inline: 'center',
|
||||
});
|
||||
*/
|
||||
|
||||
navigate(`#${activeCategory.value}`, null, { replace: true });
|
||||
}
|
||||
@@ -328,6 +325,7 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 5/3;
|
||||
border-radius: .25rem;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchTags, fetchTagsById } from '#/src/tags.js';
|
||||
import { censor } from '#/src/censor.js';
|
||||
|
||||
const tagSlugs = {
|
||||
popular: [
|
||||
@@ -117,7 +118,7 @@ const tagSlugs = {
|
||||
};
|
||||
|
||||
async function searchTags(pageContext) {
|
||||
const tags = await fetchTags({ query: pageContext.urlParsed.search.q });
|
||||
const tags = await fetchTags({ query: pageContext.urlParsed.search.q }, { restriction: pageContext.restriction });
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
@@ -136,13 +137,13 @@ export async function onBeforeRender(pageContext) {
|
||||
return searchTags(pageContext);
|
||||
}
|
||||
|
||||
const tags = await fetchTagsById(Object.values(tagSlugs).flat());
|
||||
const tags = await fetchTagsById(Object.values(tagSlugs).flat(), {}, pageContext.user, { restriction: pageContext.restriction });
|
||||
|
||||
const filteredTags = tags.filter((tag) => !pageContext.tagFilter.includes(tag.name) && !pageContext.tagFilter.includes(tag.slug));
|
||||
const tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag]));
|
||||
|
||||
const tagShowcase = Object.fromEntries(Object.entries(tagSlugs).map(([category, categorySlugs]) => [
|
||||
category,
|
||||
censor(category, pageContext.restriction),
|
||||
categorySlugs.map((slug) => tagsBySlug[slug]).filter(Boolean),
|
||||
]));
|
||||
|
||||
|
||||
@@ -17,11 +17,17 @@
|
||||
v-html="description"
|
||||
/>
|
||||
|
||||
<Photos
|
||||
v-if="tag.poster || tag.photos.length > 0"
|
||||
:tag="tag"
|
||||
/>
|
||||
|
||||
<Domains
|
||||
:path="`/tag/${tag.slug}`"
|
||||
:domains="['scenes', 'movies']"
|
||||
:domain="domain"
|
||||
class="domains-bar"
|
||||
:class="{ light: tag.poster || tag.photos.length }"
|
||||
/>
|
||||
|
||||
<Scenes v-if="domain === 'scenes'" />
|
||||
@@ -33,6 +39,7 @@
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import Photos from '#/components/tags/photos.vue';
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
import Movies from '#/components/movies/movies.vue';
|
||||
import Domains from '#/components/domains/domains.vue';
|
||||
@@ -69,6 +76,7 @@ const domain = pageContext.routeParams.domain;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .25rem 1rem;
|
||||
margin-bottom: -1px; /* for some reason there's a gap between description */
|
||||
color: var(--text-light);
|
||||
background: var(--grey-dark-40);
|
||||
}
|
||||
@@ -94,4 +102,9 @@ const domain = pageContext.routeParams.domain;
|
||||
background: var(--grey-dark-40);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.domains-bar.light {
|
||||
background: var(--background-base-10);
|
||||
border-bottom: solid 1px var(--shadow-weak-40);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,9 @@ async function fetchReleases(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchScenes(await curateScenesQuery({
|
||||
@@ -33,18 +35,21 @@ async function fetchReleases(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const tagSlug = pageContext.routeParams.tagSlug;
|
||||
|
||||
const [[tag], tagReleases, campaigns] = await Promise.all([
|
||||
fetchTagsById([tagSlug], {}, pageContext.user),
|
||||
fetchTagsById([tagSlug], {}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchReleases(pageContext),
|
||||
getRandomCampaigns([
|
||||
{ tagSlugs: [tagSlug], minRatio: 1.5 },
|
||||
{ tagSlugs: [tagSlug], minRatio: 1.5 },
|
||||
{ tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 },
|
||||
{ tagSlugs: [tagSlug], minRatio: 3 },
|
||||
{ tagSlugs: [tagSlug], minRatio: 3 },
|
||||
pageContext.routeParams.domain === 'scenes'
|
||||
? { tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 }
|
||||
: null,
|
||||
@@ -55,7 +60,7 @@ export async function onBeforeRender(pageContext) {
|
||||
const description = tag.description && md.renderInline(tag.description);
|
||||
|
||||
const campaignIndex = getCampaignIndex(releases.length);
|
||||
const [metaCampaign, paginationCampaign, sceneCampaign] = campaigns;
|
||||
const [photosCampaign, metaCampaign, paginationCampaign, sceneCampaign] = campaigns;
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
@@ -67,6 +72,7 @@ export async function onBeforeRender(pageContext) {
|
||||
},
|
||||
campaigns: {
|
||||
index: campaignIndex,
|
||||
photos: photosCampaign,
|
||||
meta: metaCampaign,
|
||||
scenes: releases.length > 5 && sceneCampaign,
|
||||
pagination: paginationCampaign,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="updates">
|
||||
<Scenes
|
||||
:show-filters="!!query"
|
||||
:show-meta="!!query"
|
||||
:show-scope-tabs="!query"
|
||||
:show-filters="false"
|
||||
:show-meta="false"
|
||||
:show-scope-tabs="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
// import { inject } from 'vue';
|
||||
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const query = Object.hasOwn(pageContext.urlParsed.search, 'q');
|
||||
// const pageContext = inject('pageContext');
|
||||
// const query = Object.hasOwn(pageContext.urlParsed.search, 'q');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -19,9 +19,9 @@ export async function onBeforeRender(pageContext) {
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
||||
aggregate: withQuery,
|
||||
dedupe: true,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
getRandomCampaigns([
|
||||
{ minRatio: 1.5 },
|
||||
{ minRatio: 2.0, maxRatio: 5 },
|
||||
{ minRatio: 0.75, maxRatio: 1.25 },
|
||||
{ minRatio: 1.5 },
|
||||
], { tagFilter: pageContext.tagFilter }),
|
||||
|
||||
@@ -49,6 +49,13 @@
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'revisions' && domain === 'actors' }"
|
||||
>Actor Revisions</a>
|
||||
|
||||
<a
|
||||
v-if="profile.isIdentityVerified"
|
||||
:href="`/user/${profile.username}/api`"
|
||||
class="domain nolink"
|
||||
:class="{ active: section === 'api' }"
|
||||
>API Keys</a>
|
||||
</nav>
|
||||
|
||||
<Stashes v-if="section === 'stashes'" />
|
||||
@@ -58,6 +65,10 @@
|
||||
v-if="section === 'templates' && profile.id === user?.id"
|
||||
:release="mockupRelease"
|
||||
/>
|
||||
|
||||
<ApiKeys
|
||||
v-if="section === 'api'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -78,6 +89,9 @@ import Stashes from '#/components/stashes/stashes.vue';
|
||||
import Alerts from '#/components/alerts/alerts.vue';
|
||||
import Summaries from '#/components/scenes/summaries.vue';
|
||||
import Revisions from '#/components/edit/revisions.vue';
|
||||
import ApiKeys from '#/components/user/api-keys.vue';
|
||||
|
||||
import mockupRelease from '#/assets/mockup-release.ts';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
@@ -87,38 +101,6 @@ const domain = pageContext.routeParams.domain;
|
||||
const user = pageContext.user;
|
||||
const profile = ref(pageContext.pageProps.profile);
|
||||
|
||||
const mockupRelease = {
|
||||
id: 1,
|
||||
title: 'Nut For Human Consumption',
|
||||
effectiveDate: new Date(),
|
||||
actors: [
|
||||
{
|
||||
name: 'Chanel Chakra',
|
||||
gender: 'female',
|
||||
},
|
||||
{
|
||||
name: 'Mo The Fucker',
|
||||
gender: 'male',
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{ name: 'anal' },
|
||||
{ name: 'facefucking' },
|
||||
{ name: 'deepthroat' },
|
||||
{ name: 'blowjob' },
|
||||
{ name: 'facial' },
|
||||
],
|
||||
movies: [{
|
||||
title: 'Best Of Traxxx 23',
|
||||
}],
|
||||
channel: {
|
||||
name: 'Traxxxed',
|
||||
},
|
||||
network: {
|
||||
name: 'Traxxx',
|
||||
},
|
||||
};
|
||||
|
||||
function scrollHorizontal(event) {
|
||||
event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
@@ -164,6 +146,7 @@ function scrollHorizontal(event) {
|
||||
.profile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
margin: 0 .5rem;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
|
||||
import { fetchUser } from '#/src/users.js';
|
||||
import { fetchUserKeys } from '#/src/auth.js';
|
||||
import { fetchUserStashes } from '#/src/stashes.js';
|
||||
import { fetchAlerts } from '#/src/alerts.js';
|
||||
import { fetchSceneRevisions } from '#/src/scenes.js';
|
||||
@@ -29,12 +30,15 @@ async function fetchRevisions(pageContext) {
|
||||
}
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [profile, alerts, userRevisions] = await Promise.all([
|
||||
const [profile, alerts, userRevisions, keys] = await Promise.all([
|
||||
fetchUser(pageContext.routeParams.username, {}, pageContext.user),
|
||||
pageContext.routeParams.section === 'alerts' && pageContext.routeParams.username === pageContext.user?.username
|
||||
? fetchAlerts(pageContext.user)
|
||||
: [],
|
||||
fetchRevisions(pageContext),
|
||||
pageContext.routeParams.section === 'api'
|
||||
? fetchUserKeys(pageContext.user)
|
||||
: [],
|
||||
]);
|
||||
|
||||
if (!profile) {
|
||||
@@ -49,8 +53,6 @@ export async function onBeforeRender(pageContext) {
|
||||
avatars,
|
||||
} = userRevisions;
|
||||
|
||||
console.log(userRevisions);
|
||||
|
||||
const stashes = await fetchUserStashes(profile.id, pageContext.user);
|
||||
|
||||
return {
|
||||
@@ -61,6 +63,7 @@ export async function onBeforeRender(pageContext) {
|
||||
stashes,
|
||||
alerts,
|
||||
revisions,
|
||||
keys,
|
||||
actors,
|
||||
tags,
|
||||
movies,
|
||||
|
||||
@@ -9,5 +9,6 @@ export default {
|
||||
'assets',
|
||||
'campaigns',
|
||||
'meta',
|
||||
'restriction',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ async function onRenderHtml(pageContext) {
|
||||
|
||||
<meta name="description" content="Keep track of new porn releases and re-discover classics from your favorite porn stars and sites" />
|
||||
|
||||
${config.analytics.enabled ? dangerouslySkipEscape(`<script src="${config.analytics.address}" data-website-id="${config.analytics.siteId}" async></script>`) : ''}
|
||||
${config.analytics.enabled ? dangerouslySkipEscape(`<script src="${config.analytics.address}" data-website-id="${config.analytics.siteId}" data-exclude-hash="true" async></script>`) : ''}
|
||||
|
||||
<title>${title}</title>
|
||||
</head>
|
||||
|
||||
@@ -19,7 +19,7 @@ import { curateStash } from './stashes.js';
|
||||
import escape from '../utils/escape-manticore.js';
|
||||
import slugify from '../utils/slugify.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { interpolateProfiles } from '../common/actors.mjs'; // eslint-disable-line import/namespace
|
||||
import { interpolateProfiles, platformsByHostname } from '../common/actors.mjs'; // eslint-disable-line import/namespace
|
||||
import { resolvePlace } from '../common/geo.mjs'; // eslint-disable-line import/namespace
|
||||
|
||||
const logger = initLogger();
|
||||
@@ -122,7 +122,10 @@ export function curateActor(actor, context = {}) {
|
||||
state: actor.residence_state,
|
||||
},
|
||||
agency: actor.agency,
|
||||
avatar: curateMedia(actor.avatar),
|
||||
avatar: actor.avatar && curateMedia({
|
||||
...actor.avatar,
|
||||
sfw_media: actor.sfw_avatar,
|
||||
}),
|
||||
socials: context.socials?.map((social) => ({
|
||||
id: social.id,
|
||||
url: social.url,
|
||||
@@ -183,6 +186,7 @@ export function sortActorsByGender(actors, context = {}) {
|
||||
const genderActors = ['transsexual', 'female', undefined, null, 'male'].flatMap((gender) => alphaActors.filter((actor) => actor.gender === gender));
|
||||
|
||||
const titleSlug = slugify(context.title);
|
||||
|
||||
const titleActors = titleSlug ? genderActors.sort((actorA, actorB) => {
|
||||
const actorASlug = actorA.slug.split('-')[0];
|
||||
const actorBSlug = actorB.slug.split('-')[0];
|
||||
@@ -213,18 +217,21 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
'residence_countries.alpha2 as residence_country_alpha2',
|
||||
knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
|
||||
knex.raw('row_to_json(entities) as entity'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||
)
|
||||
.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id')
|
||||
.leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors.birth_country_alpha2')
|
||||
.leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors.residence_country_alpha2')
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'actors.entity_id')
|
||||
.whereIn('actors.id', actorIds)
|
||||
.modify((builder) => {
|
||||
if (options.order) {
|
||||
builder.orderBy(...options.order);
|
||||
}
|
||||
}),
|
||||
})
|
||||
.groupBy('actors.id', 'avatars.id', 'sfw_media.id', 'entities.id', 'actors_meta.stashed', 'birth_countries.alpha2', 'residence_countries.alpha2'),
|
||||
knex('actors_profiles')
|
||||
.select(
|
||||
'actors_profiles.*',
|
||||
@@ -244,10 +251,12 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
'media.*',
|
||||
'actors_avatars.actor_id',
|
||||
knex.raw('json_agg(actors_avatars.profile_id) as profile_ids'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_media'),
|
||||
)
|
||||
.whereIn('actor_id', actorIds)
|
||||
.leftJoin('media', 'media.id', 'actors_avatars.media_id')
|
||||
.groupBy('media.id', 'actors_avatars.actor_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'sfw_media.id', 'actors_avatars.actor_id')
|
||||
.orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'),
|
||||
knex('actors_socials')
|
||||
.whereIn('actor_id', actorIds),
|
||||
@@ -824,16 +833,6 @@ function convertWeight(weight, units) {
|
||||
return Number(weight) || null;
|
||||
}
|
||||
|
||||
const platformsByHostname = Object.fromEntries(Object.entries(config.socials.urls).map(([platform, url]) => {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
|
||||
return [hostname, {
|
||||
platform,
|
||||
pathname: decodeURIComponent(pathname),
|
||||
url,
|
||||
}];
|
||||
}));
|
||||
|
||||
function curateSocials(socials) {
|
||||
return socials.map((social) => {
|
||||
if (!social.handle && !social.url) {
|
||||
|
||||
148
src/affiliates.js
Normal file
148
src/affiliates.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import format from 'template-format';
|
||||
|
||||
function getWatchUrl(scene) {
|
||||
try {
|
||||
if (scene.url) {
|
||||
return new URL(scene.url).href;
|
||||
}
|
||||
|
||||
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network')) {
|
||||
return new URL(scene.channel.url).href;
|
||||
}
|
||||
|
||||
if (scene.network) {
|
||||
return new URL(scene.network.url).href;
|
||||
}
|
||||
} catch (_error) {
|
||||
// invalid URL
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAffiliateSceneUrl(scene) {
|
||||
const watchUrl = getWatchUrl(scene);
|
||||
|
||||
if (!watchUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!scene.affiliate || scene.affiliate.parameters.scene === false) {
|
||||
return watchUrl;
|
||||
}
|
||||
|
||||
if (scene.affiliate.parameters.dynamicScene) {
|
||||
const scenePath = new URL(watchUrl).pathname;
|
||||
|
||||
return format(scene.affiliate.parameters.dynamicScene, {
|
||||
scenePath: scene.affiliate.parameters.prefixSlash === false
|
||||
? scenePath.replace(/^\//, '')
|
||||
: scenePath,
|
||||
entryId: scene.entryId,
|
||||
});
|
||||
}
|
||||
|
||||
if (scene.affiliate.parameters.query) { // used by e.g. Bang
|
||||
const newParams = new URLSearchParams({
|
||||
...Object.fromEntries(new URL(watchUrl).searchParams),
|
||||
...Object.fromEntries(new URLSearchParams(scene.affiliate.parameters.query)),
|
||||
});
|
||||
|
||||
return `${watchUrl}?${newParams.toString()}`;
|
||||
}
|
||||
|
||||
const affiliateUrl = scene.affiliate.parameters.replaceScene?.hostname === new URL(watchUrl).hostname
|
||||
? scene.affiliate.parameters.replaceScene.url
|
||||
: scene.affiliate.url;
|
||||
|
||||
if (!affiliateUrl) {
|
||||
return watchUrl;
|
||||
}
|
||||
|
||||
// NATS deep URL
|
||||
if (affiliateUrl.includes('/track')
|
||||
&& scene.affiliate.parameters.scene !== false
|
||||
&& (!scene.channel.isIndependent || scene.channel.id === scene.affiliate.entityId)) {
|
||||
const { pathname, search } = new URL(watchUrl);
|
||||
|
||||
return `${affiliateUrl}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
||||
}
|
||||
|
||||
const affiliateUrlComponents = new URL(affiliateUrl);
|
||||
|
||||
// NetFame / GammaE deep URL
|
||||
if (affiliateUrlComponents.searchParams.has('pa') && affiliateUrlComponents.searchParams.has('ar')) {
|
||||
affiliateUrlComponents.searchParams.set('pa', 'clip');
|
||||
affiliateUrlComponents.searchParams.set('ar', scene.entryId);
|
||||
|
||||
return affiliateUrlComponents.href;
|
||||
}
|
||||
|
||||
return watchUrl;
|
||||
}
|
||||
|
||||
function getEntityUrl(entity) {
|
||||
try {
|
||||
return new URL(entity.url || entity.parent?.url).href;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAffiliateEntityUrl(entity, affiliate) {
|
||||
const entityUrl = getEntityUrl(entity);
|
||||
const entityAffiliate = affiliate || entity.affiliate;
|
||||
|
||||
if (!entityUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entityAffiliate) {
|
||||
return entityUrl;
|
||||
}
|
||||
|
||||
const affiliateUrl = entityAffiliate.parameters?.replaceEntity?.hostname === new URL(entityUrl).hostname
|
||||
? entityAffiliate.parameters.replaceEntity.url
|
||||
: entityAffiliate.url;
|
||||
|
||||
if (affiliateUrl && (entity.id === entityAffiliate.entityId || entityUrl === entity.parent?.url)) {
|
||||
return affiliateUrl;
|
||||
}
|
||||
|
||||
if (entityAffiliate.parameters?.query) {
|
||||
const newParams = new URLSearchParams({
|
||||
...Object.fromEntries(new URL(entityUrl).searchParams),
|
||||
...Object.fromEntries(new URLSearchParams(entityAffiliate.parameters.query)),
|
||||
});
|
||||
|
||||
return `${entityUrl}?${newParams.toString()}`;
|
||||
}
|
||||
|
||||
if (entity.type === 'network' || entity.isIndependent) {
|
||||
return entityUrl;
|
||||
}
|
||||
|
||||
// channel has its own domain
|
||||
if (new URL(entityUrl).pathname === '/' && entityUrl !== entity.parent?.url) {
|
||||
return entityUrl;
|
||||
}
|
||||
|
||||
if (entityAffiliate.parameters.dynamicEntity) {
|
||||
const entityPath = new URL(entityUrl).pathname;
|
||||
|
||||
return format(entityAffiliate.parameters.dynamicEntity, {
|
||||
entityPath: entityAffiliate.parameters.prefixSlash
|
||||
? entityPath
|
||||
: entityPath.replace(/^\//, ''),
|
||||
});
|
||||
}
|
||||
|
||||
if (affiliateUrl?.includes('/track')
|
||||
&& entityAffiliate.parameters.channel !== false) {
|
||||
const { pathname, search } = new URL(entityUrl);
|
||||
|
||||
return `${affiliateUrl}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
||||
}
|
||||
|
||||
return entityUrl;
|
||||
}
|
||||
109
src/alerts.js
109
src/alerts.js
@@ -2,6 +2,7 @@ import escapeRegexp from 'escape-string-regexp';
|
||||
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import { fetchScenesById } from './scenes.js';
|
||||
import { getIdsBySlug } from './cache.js';
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
import { HttpError } from './errors.js';
|
||||
|
||||
@@ -46,6 +47,8 @@ function curateAlert(alert, context = {}) {
|
||||
slug: stash.stash_slug,
|
||||
isPrimary: stash.stash_primary,
|
||||
})) || [],
|
||||
comment: alert.comment,
|
||||
meta: alert.meta,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,7 +136,12 @@ export async function createAlert(alert, reqUser) {
|
||||
throw new HttpError('You are not authenthicated', 401);
|
||||
}
|
||||
|
||||
if ((!alert.actors || alert.actors.length === 0) && (!alert.tags || alert.tags.length === 0) && (!alert.entities || alert.entities.length === 0) && (!alert.matches || alert.matches.length === 0)) {
|
||||
const tagIds = (await getIdsBySlug(alert.tags, 'tags') || []).concat(alert.tagIds || []);
|
||||
const entityIds = (await getIdsBySlug(alert.entities || [], 'entities')).concat(alert.entityIds || []);
|
||||
const actorIds = [...(alert.actors || []), ...(alert.actorIds || [])]; // for consistency with tagIds and entityIds
|
||||
const stashIds = [...(alert.stashes || []), ...(alert.stashIds || [])]; // for consistency with tagIds and entityIds
|
||||
|
||||
if (actorIds.length === 0 && tagIds.length === 0 && entityIds.length === 0 && (!alert.matches || alert.matches.length === 0)) {
|
||||
throw new HttpError('Alert must contain at least one actor, tag or entity', 400);
|
||||
}
|
||||
|
||||
@@ -141,6 +149,14 @@ export async function createAlert(alert, reqUser) {
|
||||
throw new HttpError('Match must define a property and an expression', 400);
|
||||
}
|
||||
|
||||
if (tagIds.length < (alert.tags?.length || 0) + (alert.tagIds?.length || 0)) {
|
||||
throw new HttpError('Failed to resolve all tags');
|
||||
}
|
||||
|
||||
if (entityIds.length < (alert.entities?.length || 0) + (alert.entityIds?.length || 0)) {
|
||||
throw new HttpError('Failed to resolve all entities');
|
||||
}
|
||||
|
||||
const [{ id: alertId }] = await knex('alerts')
|
||||
.insert({
|
||||
user_id: reqUser.id,
|
||||
@@ -152,15 +168,17 @@ export async function createAlert(alert, reqUser) {
|
||||
all_tags: alert.allTags,
|
||||
all_matches: alert.allMatches,
|
||||
from_preset: alert.preset,
|
||||
comment: alert.comment,
|
||||
meta: alert.meta,
|
||||
})
|
||||
.returning('id');
|
||||
|
||||
await Promise.all([
|
||||
alert.actors?.length > 0 && knex('alerts_actors').insert(alert.actors.map((actorId) => ({
|
||||
actorIds?.length > 0 && knex('alerts_actors').insert(actorIds.map((actorId) => ({
|
||||
alert_id: alertId,
|
||||
actor_id: actorId,
|
||||
}))),
|
||||
alert.tags?.length > 0 && knex('alerts_tags').insert(alert.tags.map((tagId) => ({
|
||||
tagIds?.length > 0 && knex('alerts_tags').insert(tagIds.map((tagId) => ({
|
||||
alert_id: alertId,
|
||||
tag_id: tagId,
|
||||
}))),
|
||||
@@ -171,11 +189,11 @@ export async function createAlert(alert, reqUser) {
|
||||
? match.expression.slice(1, -1)
|
||||
: escapeRegexp(match.expression),
|
||||
}))),
|
||||
alert.stashes?.length > 0 && knex('alerts_stashes').insert(alert.stashes.map((stashId) => ({
|
||||
stashIds?.length > 0 && knex('alerts_stashes').insert(stashIds.map((stashId) => ({
|
||||
alert_id: alertId,
|
||||
stash_id: stashId,
|
||||
}))),
|
||||
alert.entities?.length > 0 && knex('alerts_entities').insert(alert.entities.map((entityId) => ({
|
||||
entityIds?.length > 0 && knex('alerts_entities').insert(entityIds.map((entityId) => ({
|
||||
alert_id: alertId,
|
||||
entity_id: entityId,
|
||||
})).slice(0, alert.allEntities ? 1 : Infinity)), // one scene can never match multiple entities in AND mode
|
||||
@@ -195,6 +213,7 @@ export async function createAlert(alert, reqUser) {
|
||||
export async function removeAlert(alertId, reqUser) {
|
||||
const alert = await knex('alerts')
|
||||
.where('alerts.id', alertId)
|
||||
.where('alerts.user_id', reqUser.id)
|
||||
.select(
|
||||
'alerts.id',
|
||||
knex.raw('coalesce(array_agg(distinct alerts_actors.actor_id) filter (where alerts_actors.actor_id is not null), \'{}\') as actor_ids'),
|
||||
@@ -208,16 +227,51 @@ export async function removeAlert(alertId, reqUser) {
|
||||
.groupBy('alerts.id')
|
||||
.first();
|
||||
|
||||
await knex('alerts')
|
||||
if (!alert) {
|
||||
throw new HttpError(`Could not find alert ${alertId}`, 404);
|
||||
}
|
||||
|
||||
const [removed] = await knex('alerts')
|
||||
.where('id', alertId)
|
||||
.where('user_id', reqUser.id)
|
||||
.delete();
|
||||
.delete()
|
||||
.returning('*');
|
||||
|
||||
await Promise.all([
|
||||
if (!removed) {
|
||||
throw new HttpError(`Could not remove alert ${alertId}`, 404);
|
||||
}
|
||||
|
||||
// slow not critical for response, don't await
|
||||
Promise.all([
|
||||
alert.actor_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'),
|
||||
alert.tag_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'),
|
||||
alert.entity_ids?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'),
|
||||
]);
|
||||
|
||||
return curateAlert(removed);
|
||||
}
|
||||
|
||||
function curateNotification(notification, scenes) {
|
||||
const scene = scenes.find((sceneX) => sceneX.id === notification.scene_id);
|
||||
|
||||
return {
|
||||
id: notification.id,
|
||||
sceneId: notification.scene_id,
|
||||
scene,
|
||||
alertId: notification.alert_id,
|
||||
matchedActors: scene.actors.filter((actor) => notification.alert_actors.includes(actor.id)),
|
||||
matchedTags: scene.tags.filter((tag) => notification.alert_tags.includes(tag.id)),
|
||||
matchedEntity: [scene.channel, scene.network].find((entity) => notification.alert_entities.includes(entity?.id)) || null,
|
||||
matchedExpressions: notification.alert_matches
|
||||
.filter((match) => new RegExp(match.expression, 'ui').test(scene[match.property]))
|
||||
.map((match) => ({
|
||||
id: match.id,
|
||||
property: match.property,
|
||||
expression: match.expression,
|
||||
})),
|
||||
isSeen: notification.seen,
|
||||
createdAt: notification.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchUnseenNotificationsCount(reqUser) {
|
||||
@@ -262,29 +316,7 @@ export async function fetchNotifications(reqUser, options = {}) {
|
||||
]);
|
||||
|
||||
const scenes = await fetchScenesById(notifications.map((notification) => notification.scene_id));
|
||||
|
||||
const curatedNotifications = notifications.map((notification) => {
|
||||
const scene = scenes.find((sceneX) => sceneX.id === notification.scene_id);
|
||||
|
||||
return {
|
||||
id: notification.id,
|
||||
sceneId: notification.scene_id,
|
||||
scene,
|
||||
alertId: notification.alert_id,
|
||||
matchedActors: scene.actors.filter((actor) => notification.alert_actors.includes(actor.id)),
|
||||
matchedTags: scene.tags.filter((tag) => notification.alert_tags.includes(tag.id)),
|
||||
matchedEntity: [scene.channel, scene.network].find((entity) => notification.alert_entities.includes(entity?.id)) || null,
|
||||
matchedExpressions: notification.alert_matches
|
||||
.filter((match) => new RegExp(match.expression, 'ui').test(scene[match.property]))
|
||||
.map((match) => ({
|
||||
id: match.id,
|
||||
property: match.property,
|
||||
expression: match.expression,
|
||||
})),
|
||||
isSeen: notification.seen,
|
||||
createdAt: notification.created_at,
|
||||
};
|
||||
});
|
||||
const curatedNotifications = notifications.map((notification) => curateNotification(notification, scenes));
|
||||
|
||||
return {
|
||||
notifications: curatedNotifications,
|
||||
@@ -293,18 +325,27 @@ export async function fetchNotifications(reqUser, options = {}) {
|
||||
}
|
||||
|
||||
export async function updateNotification(notificationId, updatedNotification, reqUser) {
|
||||
await knex('notifications')
|
||||
const [updated] = await knex('notifications')
|
||||
.where('id', notificationId)
|
||||
.where('user_id', reqUser.id)
|
||||
.update({
|
||||
seen: updatedNotification.seen,
|
||||
});
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
if (!updated) {
|
||||
throw new HttpError(`No notification ${notificationId} found to update`, 404);
|
||||
}
|
||||
|
||||
return updated.id;
|
||||
}
|
||||
|
||||
export async function updateNotifications(updatedNotification, reqUser) {
|
||||
await knex('notifications')
|
||||
const updatedCount = await knex('notifications')
|
||||
.where('user_id', reqUser.id)
|
||||
.update({
|
||||
seen: updatedNotification.seen,
|
||||
});
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
10
src/argv.js
10
src/argv.js
@@ -1,11 +1,15 @@
|
||||
import yargs from 'yargs';
|
||||
|
||||
const { argv } = yargs()
|
||||
.command('npm start')
|
||||
const { argv } = yargs(process.argv.slice(2))
|
||||
.option('debug', {
|
||||
describe: 'Show error stack traces',
|
||||
describe: 'Show error stack traces and inputs',
|
||||
type: 'boolean',
|
||||
default: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
.option('ip', {
|
||||
describe: 'Mock IP address',
|
||||
type: 'string',
|
||||
default: null,
|
||||
});
|
||||
|
||||
export default argv;
|
||||
|
||||
12
src/auth.js
12
src/auth.js
@@ -5,6 +5,7 @@ import fs from 'fs/promises';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { shapes } from '@dicebear/collection';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verify } from 'hcaptcha';
|
||||
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import redis from './redis.js';
|
||||
@@ -105,6 +106,15 @@ export async function signup(credentials, userIp) {
|
||||
throw new HttpError('Password must be 3 characters or longer', 400);
|
||||
}
|
||||
|
||||
if (config.auth.captcha.enabled) {
|
||||
const captchaVerification = await verify(config.auth.captcha.secretKey, credentials.captcha);
|
||||
|
||||
if (!captchaVerification.success) {
|
||||
logger.warn(`Invalid sign-up CAPTCHA from '${curatedUsername}' (${credentials.email}, ${userIp})`);
|
||||
throw new HttpError('Invalid CAPTCHA', 400);
|
||||
}
|
||||
}
|
||||
|
||||
const existingUser = await knex('users')
|
||||
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
||||
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
||||
@@ -134,7 +144,7 @@ export async function signup(credentials, userIp) {
|
||||
primary: true,
|
||||
});
|
||||
|
||||
logger.verbose(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
||||
logger.info(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
||||
|
||||
await generateAvatar({
|
||||
id: userId,
|
||||
|
||||
@@ -10,11 +10,9 @@ export async function getIdsBySlug(slugs, domain, toMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* this, naturally, fails if the slug is 69 etc.
|
||||
if (Number(slug)) {
|
||||
return Number(slug); // already an ID or missing
|
||||
if (typeof slug === 'number') {
|
||||
return slug; // already an ID, tags like 69 should be a string at this stage
|
||||
}
|
||||
*/
|
||||
|
||||
const id = await redis.hGet(`traxxx:${domain}:id_by_slug`, slug);
|
||||
|
||||
|
||||
@@ -4,18 +4,41 @@ import { knexOwner as knex } from './knex.js';
|
||||
import { curateEntity } from './entities.js';
|
||||
import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
function getCampaignUrl(campaign, entity) {
|
||||
if (!campaign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (campaign.url) {
|
||||
return campaign.url;
|
||||
}
|
||||
|
||||
if (campaign.affiliate?.url) {
|
||||
return campaign.affiliate.url;
|
||||
}
|
||||
|
||||
if (campaign.entity) {
|
||||
// resolve e.g. parameter tracking
|
||||
return getAffiliateEntityUrl(entity, campaign.affiliate);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function curateCampaign(campaign) {
|
||||
if (!campaign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
const entity = campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity });
|
||||
|
||||
const curatedCampaign = {
|
||||
id: campaign.id,
|
||||
url: campaign.url,
|
||||
entity: campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity }),
|
||||
entity,
|
||||
banner: campaign.banner && {
|
||||
id: campaign.banner.id,
|
||||
type: campaign.banner.type,
|
||||
@@ -31,9 +54,13 @@ function curateCampaign(campaign) {
|
||||
parameters: campaign.affiliate.parameters,
|
||||
},
|
||||
};
|
||||
|
||||
curatedCampaign.url = getCampaignUrl(campaign, entity);
|
||||
|
||||
return curatedCampaign;
|
||||
}
|
||||
|
||||
function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns, allCampaigns, options) {
|
||||
function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns) {
|
||||
if (primaryCampaigns.length > 0) {
|
||||
return primaryCampaigns[crypto.randomInt(primaryCampaigns.length)];
|
||||
}
|
||||
@@ -46,31 +73,36 @@ function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampai
|
||||
return preferredCampaigns[crypto.randomInt(preferredCampaigns.length)];
|
||||
}
|
||||
|
||||
if (allCampaigns.length > 0 && options.allowRandomFallback !== false) {
|
||||
return allCampaigns[crypto.randomInt(allCampaigns.length)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getRandomCampaign(options = {}, context = {}) {
|
||||
export async function getRandomCampaign(options = {}, context = {}, pass = 0) {
|
||||
const campaigns = options.campaigns
|
||||
|| await redis.hGetAll('traxxx:campaigns').then((rawCampaigns) => Object.values(rawCampaigns).map((rawCampaign) => JSON.parse(rawCampaign)));
|
||||
|
||||
const validCampaigns = campaigns.filter((campaign) => {
|
||||
if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
|
||||
// too small
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
|
||||
// too big
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) {
|
||||
// this is an entity page, this campaign does not belong to this entity
|
||||
return false;
|
||||
}
|
||||
|
||||
if (campaign.affiliate?.parameters?.global === false && !options.entityIds) {
|
||||
// this campaign should only show on entity page
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
|
||||
// wrong tag
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,8 +114,6 @@ export async function getRandomCampaign(options = {}, context = {}) {
|
||||
return true;
|
||||
});
|
||||
|
||||
// console.log(validCampaigns);
|
||||
|
||||
const campaignsByEntityId = validCampaigns.reduce((acc, campaign) => {
|
||||
const entityId = campaign.entity.parent?.id || campaign.entity.id;
|
||||
|
||||
@@ -103,6 +133,18 @@ export async function getRandomCampaign(options = {}, context = {}) {
|
||||
|
||||
const randomCampaign = selectRandomCampaign(primaryCampaigns, randomEntityCampaigns, validCampaigns, campaigns, options);
|
||||
|
||||
// no campaign found, gradually widen scope
|
||||
if (!randomCampaign && pass === 0 && options.allowRandomFallback !== false) {
|
||||
return getRandomCampaign({
|
||||
minRatio: options.minRatio,
|
||||
maxRatio: options.maxRatio,
|
||||
}, context, pass + 1);
|
||||
}
|
||||
|
||||
if (!randomCampaign && pass === 1 && options.allowRandomFallback !== false && options.allowRandomRatio) {
|
||||
return getRandomCampaign({}, context, pass + 1);
|
||||
}
|
||||
|
||||
return randomCampaign;
|
||||
}
|
||||
|
||||
|
||||
55
src/censor.js
Normal file
55
src/censor.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import config from 'config';
|
||||
|
||||
import {
|
||||
TextCensor,
|
||||
RegExpMatcher,
|
||||
englishDataset,
|
||||
englishRecommendedTransformers,
|
||||
DataSet,
|
||||
pattern,
|
||||
// asteriskCensorStrategy,
|
||||
} from 'obscenity';
|
||||
|
||||
const textCensor = new TextCensor();
|
||||
|
||||
// built-in asterisk strategy replaces entire word
|
||||
textCensor.setStrategy(({
|
||||
input,
|
||||
startIndex,
|
||||
endIndex,
|
||||
matchLength,
|
||||
}) => {
|
||||
if (matchLength <= 2) {
|
||||
return '*'.repeat(matchLength);
|
||||
}
|
||||
|
||||
return `${input.at(startIndex)}${'*'.repeat(matchLength - 2)}${input.at(endIndex)}`;
|
||||
});
|
||||
|
||||
const dataset = new DataSet().addAll(englishDataset);
|
||||
|
||||
config.restrictions.censors.forEach((word) => {
|
||||
dataset.addPhrase((phrase) => phrase
|
||||
.setMetadata({ originalWord: word })
|
||||
.addPattern(pattern`${word}`));
|
||||
});
|
||||
|
||||
const matcher = new RegExpMatcher({
|
||||
...dataset.build(),
|
||||
...englishRecommendedTransformers,
|
||||
});
|
||||
|
||||
export function censor(text, restriction) {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!restriction) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const censorMatches = matcher.getAllMatches(text);
|
||||
const censoredText = textCensor.applyTo(text, censorMatches);
|
||||
|
||||
return censoredText;
|
||||
}
|
||||
@@ -2,28 +2,42 @@ import knex from './knex.js';
|
||||
import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import entityPrefixes from './entities-prefixes.js';
|
||||
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
export function curateEntity(entity, context) {
|
||||
export function curateEntity(entity, context = {}) {
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
const curatedEntity = {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
name: censor(entity.name, context.restriction),
|
||||
slug: entity.slug,
|
||||
type: entity.type,
|
||||
url: entity.url,
|
||||
isIndependent: entity.independent,
|
||||
hasLogo: entity.has_logo,
|
||||
hasLogo: context.restriction ? false : entity.has_logo,
|
||||
parent: curateEntity(entity.parent, context),
|
||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({ ...child, parent: entity }, { parent: entity })) || [],
|
||||
tags: context?.tags?.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
})),
|
||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({
|
||||
...child,
|
||||
parent: entity,
|
||||
}, {
|
||||
parent: entity,
|
||||
restriction: context.restriction,
|
||||
})) || [],
|
||||
affiliate: entity.affiliate ? {
|
||||
id: entity.affiliate.id,
|
||||
entityId: entity.affiliate.entity_id,
|
||||
url: entity.affiliate.url,
|
||||
parameters: entity.affiliate.parameters,
|
||||
parameters: entity.affiliate.parameters || {},
|
||||
} : null,
|
||||
...context?.append?.[entity.id],
|
||||
alerts: {
|
||||
@@ -31,9 +45,13 @@ export function curateEntity(entity, context) {
|
||||
multi: context?.alerts?.filter((alert) => !alert.is_only).flatMap((alert) => alert.alert_ids) || [],
|
||||
},
|
||||
};
|
||||
|
||||
curatedEntity.affiliateUrl = getAffiliateEntityUrl(curatedEntity);
|
||||
|
||||
return curatedEntity;
|
||||
}
|
||||
|
||||
export async function fetchEntities(options = {}) {
|
||||
export async function fetchEntities(options = {}, context = {}) {
|
||||
const entities = await knex('entities')
|
||||
.select('entities.*', knex.raw('row_to_json(parents) as parent'))
|
||||
.modify((builder) => {
|
||||
@@ -43,7 +61,8 @@ export async function fetchEntities(options = {}) {
|
||||
.where((subBuilder) => {
|
||||
subBuilder
|
||||
.whereILike('entities.name', `%${options.query}%`)
|
||||
.orWhereILike('entities.slug', `%${options.query}%`);
|
||||
.orWhereILike('entities.slug', `%${options.query}%`)
|
||||
.orWhereILike(knex.raw('array_to_string(entities.alias, \',\', \'*\')'), `%${options.query}%`);
|
||||
})
|
||||
.whereNot('entities.type', 'info');
|
||||
});
|
||||
@@ -75,30 +94,47 @@ export async function fetchEntities(options = {}) {
|
||||
.offset((options.page - 1) * options.limit)
|
||||
.limit(options.limit || 1000);
|
||||
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry));
|
||||
const entitiesTags = await knex('entities_tags')
|
||||
.select('entity_id', 'tags.*')
|
||||
.leftJoin('tags', 'tags.id', 'tag_id')
|
||||
.whereIn('entity_id', entities.map((entity) => entity.id));
|
||||
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||
...context,
|
||||
tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
const [entities, children, alerts] = await Promise.all([
|
||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser, context) {
|
||||
const [entities, children, tags, alerts] = await Promise.all([
|
||||
knex('entities')
|
||||
.select(
|
||||
'entities.*',
|
||||
knex.raw('row_to_json(parents) as parent'),
|
||||
knex.raw('row_to_json(affiliates) as affiliate'),
|
||||
knex.raw('coalesce(row_to_json(affiliates), row_to_json(network_affiliates)) as affiliate'),
|
||||
)
|
||||
.whereIn('entities.id', entityIds)
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.leftJoin('affiliates', knex.raw('affiliates.entity_id in (entities.id, parents.id)'))
|
||||
.leftJoin('affiliates', 'affiliates.entity_id', 'entities.id')
|
||||
.leftJoin('affiliates as network_affiliates', 'network_affiliates.entity_id', 'parents.id')
|
||||
.modify((builder) => {
|
||||
if (options.order) {
|
||||
builder.orderBy(...options.order);
|
||||
}
|
||||
})
|
||||
.groupBy('entities.id', 'parents.id', 'affiliates.id'),
|
||||
.groupBy('entities.id', 'parents.id', 'affiliates.id', 'affiliates.entity_id', 'network_affiliates.id', 'network_affiliates.entity_id'),
|
||||
options.includeChildren ? knex('entities')
|
||||
.whereIn('entities.parent_id', entityIds)
|
||||
.whereNot('type', 'info')
|
||||
.orderBy('slug') : [],
|
||||
.orderBy([
|
||||
{ column: knex.raw('array_position(array[\'network\', \'channel\']::varchar[], type)'), order: 'asc' },
|
||||
{ column: 'independent', order: 'desc' },
|
||||
{ column: 'slug', order: 'asc' },
|
||||
]) : [],
|
||||
knex('entities_tags')
|
||||
.select('entity_id', 'tags.*')
|
||||
.leftJoin('tags', 'tags.id', 'tag_id')
|
||||
.whereIn('entity_id', entityIds),
|
||||
reqUser
|
||||
? knex('alerts_users_entities')
|
||||
.where('user_id', reqUser.id)
|
||||
@@ -108,6 +144,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
|
||||
if (options.order) {
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||
...context,
|
||||
append: options.append,
|
||||
children: children.filter((channel) => channel.parent_id === entityEntry.id),
|
||||
alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id),
|
||||
@@ -123,8 +160,10 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
}
|
||||
|
||||
return curateEntity(entity, {
|
||||
...context,
|
||||
append: options.append,
|
||||
children: children.filter((channel) => channel.parent_id === entity.id),
|
||||
tags: tags.filter((tag) => tag.entity_id === entity.id),
|
||||
alerts: alerts.filter((alert) => alert.entity_id === entity.id),
|
||||
});
|
||||
}).filter(Boolean);
|
||||
@@ -158,3 +197,32 @@ export async function cacheEntityIds() {
|
||||
|
||||
logger.info('Cached entity IDs by slug');
|
||||
}
|
||||
|
||||
const sortMap = {
|
||||
releases: knex.raw('count(releases.id)'),
|
||||
latest: 'latest_release_date',
|
||||
};
|
||||
|
||||
export async function fetchEntityHealths(options) {
|
||||
const entities = await knex('entities')
|
||||
.select(
|
||||
'entities.*',
|
||||
knex.raw('row_to_json(parents) as parent'),
|
||||
knex.raw('max(effective_date) as latest_release_date'),
|
||||
knex.raw('count(releases.id) as total_releases'),
|
||||
)
|
||||
.leftJoin('releases', 'releases.entity_id', 'entities.id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.orderBy(sortMap[options.sort] || options.sort || sortMap.releases, options.order || 'desc')
|
||||
.groupBy('entities.id', 'parents.id');
|
||||
|
||||
const curatedEntities = entities.map((entity) => ({
|
||||
...curateEntity(entity),
|
||||
totalReleases: entity.total_releases,
|
||||
latestReleaseDate: entity.latest_release_date,
|
||||
}));
|
||||
|
||||
return {
|
||||
entities: curatedEntities,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// import config from 'config';
|
||||
import { pageContext } from '../renderer/usePageContext.js';
|
||||
|
||||
function getBasePath(media, type, options) {
|
||||
/*
|
||||
if (store.state.ui.sfw) {
|
||||
return config.media.assetPath;
|
||||
function getBasePath(media, options) {
|
||||
if (pageContext.restriction) {
|
||||
return pageContext.env.media.assetPath;
|
||||
}
|
||||
*/
|
||||
|
||||
if (media.isS3) {
|
||||
return options.s3Path;
|
||||
@@ -20,15 +17,13 @@ function getBasePath(media, type, options) {
|
||||
}
|
||||
|
||||
function getFilename(media, type, options) {
|
||||
/*
|
||||
if (store.state.ui.sfw && type && !options?.original) {
|
||||
return media.sfw[type];
|
||||
if (pageContext.restriction && type && !options?.original) {
|
||||
return media.sfw?.[type];
|
||||
}
|
||||
|
||||
if (store.state.ui.sfw) {
|
||||
return media.sfw.path;
|
||||
if (pageContext.restriction) {
|
||||
return media.sfw?.path;
|
||||
}
|
||||
*/
|
||||
|
||||
if (type && !options?.original) {
|
||||
return media[type];
|
||||
@@ -42,7 +37,7 @@ export default function getPath(media, type, options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = getBasePath(media, type, { ...pageContext.env.media, ...options });
|
||||
const path = getBasePath(media, { ...pageContext.env.media, ...options });
|
||||
const filename = getFilename(media, type, { ...pageContext.env.media, ...options });
|
||||
|
||||
return `${path}/${filename}`;
|
||||
|
||||
11
src/knex.js
11
src/knex.js
@@ -1,15 +1,6 @@
|
||||
import config from 'config';
|
||||
import knex from 'knex';
|
||||
|
||||
export const knexQuery = knex({
|
||||
client: 'pg',
|
||||
connection: config.database.query,
|
||||
pool: config.database.pool,
|
||||
// performance overhead, don't use asyncStackTraces in production
|
||||
asyncStackTraces: process.env.NODE_ENV === 'development',
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
export const knexOwner = knex({
|
||||
client: 'pg',
|
||||
connection: config.database.owner,
|
||||
@@ -19,6 +10,8 @@ export const knexOwner = knex({
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
export const knexQuery = knexOwner; // legacy
|
||||
|
||||
export const knexManticore = knex({
|
||||
client: 'mysql',
|
||||
connection: {
|
||||
|
||||
10
src/media.js
10
src/media.js
@@ -1,3 +1,5 @@
|
||||
import { curateEntity } from './entities.js';
|
||||
|
||||
export function curateMedia(media, context = {}) {
|
||||
if (!media) {
|
||||
return null;
|
||||
@@ -16,11 +18,19 @@ export function curateMedia(media, context = {}) {
|
||||
height: media.height,
|
||||
index: media.index,
|
||||
sharpness: media.sharpness,
|
||||
entropy: media.entropy,
|
||||
credit: media.credit,
|
||||
mime: mime && {
|
||||
type: mime[0],
|
||||
subtype: mime[1],
|
||||
},
|
||||
comment: media.comment,
|
||||
entity: media.entity && curateEntity({
|
||||
...media.entity,
|
||||
parent: media.entity_parent,
|
||||
}),
|
||||
type: context.type || null,
|
||||
sfw: curateMedia(media.sfw_media),
|
||||
isRestricted: context.isRestricted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,29 +8,30 @@ import { curateMedia } from './media.js';
|
||||
import { fetchTagsById } from './tags.js';
|
||||
import { fetchEntitiesById } from './entities.js';
|
||||
import { curateStash } from './stashes.js';
|
||||
import { censor } from './censor.js';
|
||||
import escape from '../utils/escape-manticore.js';
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
|
||||
function curateMovie(rawMovie, assets) {
|
||||
function curateMovie(rawMovie, assets, context = {}) {
|
||||
if (!rawMovie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: rawMovie.id,
|
||||
title: rawMovie.title,
|
||||
title: censor(rawMovie.title, context.restriction),
|
||||
slug: rawMovie.slug,
|
||||
url: rawMovie.url,
|
||||
date: rawMovie.date,
|
||||
datePrecision: rawMovie.date_precision,
|
||||
createdAt: rawMovie.created_at,
|
||||
effectiveDate: rawMovie.effective_date,
|
||||
description: rawMovie.description,
|
||||
description: censor(rawMovie.description, context.restriction),
|
||||
duration: rawMovie.duration,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
@@ -38,7 +39,7 @@ function curateMovie(rawMovie, assets) {
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
} : null,
|
||||
@@ -51,7 +52,7 @@ function curateMovie(rawMovie, assets) {
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
})),
|
||||
// poster: curateMedia(assets.poster),
|
||||
covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })),
|
||||
@@ -64,7 +65,7 @@ function curateMovie(rawMovie, assets) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMoviesById(movieIds, reqUser) {
|
||||
export async function fetchMoviesById(movieIds, reqUser, context = {}) {
|
||||
const {
|
||||
movies,
|
||||
channels,
|
||||
@@ -123,20 +124,25 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||
.orderBy('priority', 'desc'),
|
||||
covers: knex('movies_covers')
|
||||
.select('media.*', 'movies_covers.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movie_id', movieIds)
|
||||
.leftJoin('media', 'media.id', 'movies_covers.media_id')
|
||||
.orderBy('media.index'),
|
||||
photos: knex.transaction(async (trx) => {
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.orderBy('media.index')
|
||||
.groupBy('media.id', 'movies_covers.movie_id', 'sfw_media.id'),
|
||||
photos: context.restriction ? [] : knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('movies_scenes')
|
||||
.select('media.*', 'movies_scenes.movie_id')
|
||||
.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movies_scenes.movie_id', movieIds)
|
||||
.whereNotNull('media.id')
|
||||
.leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id')
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id');
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
|
||||
}),
|
||||
caps: knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -144,11 +150,13 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
}
|
||||
|
||||
return trx('movies_scenes')
|
||||
.select('media.*', 'movies_scenes.movie_id')
|
||||
.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movies_scenes.movie_id', movieIds)
|
||||
.whereNotNull('media.id')
|
||||
.leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_id')
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id');
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
|
||||
}),
|
||||
trailers: knex('movies_trailers')
|
||||
.whereIn('movie_id', movieIds)
|
||||
@@ -192,7 +200,7 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
caps: movieCaps,
|
||||
trailer: movieTrailer,
|
||||
stashes: movieStashes,
|
||||
});
|
||||
}, context);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -287,6 +295,10 @@ async function queryManticoreSql(filters, options) {
|
||||
builder.whereRaw('any(entity_ids) = ?', filters.entityId);
|
||||
}
|
||||
|
||||
if (filters.requireCover) {
|
||||
builder.where('has_cover', true);
|
||||
}
|
||||
|
||||
if (typeof filters.isShowcased === 'boolean') {
|
||||
builder.where('is_showcased', filters.isShowcased);
|
||||
}
|
||||
@@ -394,7 +406,7 @@ function countAggregations(buckets) {
|
||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
}
|
||||
|
||||
export async function fetchMovies(filters, rawOptions, reqUser) {
|
||||
export async function fetchMovies(filters, rawOptions, reqUser, context) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
console.log(options);
|
||||
@@ -409,13 +421,13 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
|
||||
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
||||
|
||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [],
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||
]);
|
||||
|
||||
const movieIds = result.movies.map((movie) => Number(movie.id));
|
||||
const movies = await fetchMoviesById(movieIds, reqUser);
|
||||
const movies = await fetchMoviesById(movieIds, reqUser, context);
|
||||
|
||||
return {
|
||||
movies,
|
||||
|
||||
170
src/scenes.js
170
src/scenes.js
@@ -1,7 +1,7 @@
|
||||
import config from 'config';
|
||||
import util from 'util'; /* eslint-disable-line no-unused-vars */
|
||||
import { MerkleJson } from 'merkle-json';
|
||||
|
||||
import argv from './argv.js';
|
||||
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
|
||||
import { utilsApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
@@ -15,67 +15,35 @@ import escape from '../utils/escape-manticore.js';
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
import initLogger from './logger.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { getAffiliateSceneUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
|
||||
function getWatchUrl(scene) {
|
||||
if (scene.url) {
|
||||
return scene.url;
|
||||
}
|
||||
|
||||
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network')) {
|
||||
return scene.channel.url;
|
||||
}
|
||||
|
||||
if (scene.network) {
|
||||
return scene.network.url;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAffiliateUrl(scene) {
|
||||
const watchUrl = getWatchUrl(scene);
|
||||
|
||||
if (!watchUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!scene.affiliate?.parameters) {
|
||||
return scene.url;
|
||||
}
|
||||
|
||||
const newParams = new URLSearchParams({
|
||||
...Object.fromEntries(new URL(watchUrl).searchParams),
|
||||
...Object.fromEntries(new URLSearchParams(scene.affiliate.parameters)),
|
||||
});
|
||||
|
||||
return `${watchUrl}?${newParams.toString()}`;
|
||||
}
|
||||
|
||||
function curateScene(rawScene, assets) {
|
||||
function curateScene(rawScene, assets, reqUser, context) {
|
||||
if (!rawScene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedScene = {
|
||||
id: rawScene.id,
|
||||
title: rawScene.title,
|
||||
title: censor(rawScene.title, context.restriction),
|
||||
slug: rawScene.slug,
|
||||
url: rawScene.url,
|
||||
entryId: rawScene.entry_id,
|
||||
date: rawScene.date,
|
||||
datePrecision: rawScene.date_precision,
|
||||
createdAt: rawScene.created_at,
|
||||
effectiveDate: rawScene.effective_date,
|
||||
description: rawScene.description,
|
||||
description: censor(rawScene.description, context.restriction),
|
||||
duration: rawScene.duration,
|
||||
shootId: rawScene.shoot_id,
|
||||
productionDate: rawScene.production_date,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
@@ -83,7 +51,7 @@ function curateScene(rawScene, assets) {
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.network_has_logo,
|
||||
} : null,
|
||||
@@ -97,7 +65,8 @@ function curateScene(rawScene, assets) {
|
||||
affiliate: assets.channel.affiliate ? {
|
||||
id: assets.channel.affiliate.id,
|
||||
url: assets.channel.affiliate.url,
|
||||
parameters: assets.channel.affiliate.parameters,
|
||||
parameters: assets.channel.affiliate.parameters || {},
|
||||
entityId: assets.channel.affiliate.entity_id,
|
||||
} : null,
|
||||
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, {
|
||||
sceneDate: rawScene.effective_date,
|
||||
@@ -111,15 +80,19 @@ function curateScene(rawScene, assets) {
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
priority: tag.priority,
|
||||
})),
|
||||
chapters: assets.chapters.map((chapter) => ({
|
||||
id: chapter.id,
|
||||
title: chapter.title,
|
||||
description: chapter.description,
|
||||
time: chapter.time,
|
||||
date: chapter.date,
|
||||
duration: chapter.duration,
|
||||
poster: curateMedia(chapter.chapter_poster),
|
||||
poster: context.restriction
|
||||
? null
|
||||
: curateMedia(chapter.chapter_poster, { type: 'poster' }),
|
||||
tags: chapter.chapter_tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
@@ -131,27 +104,44 @@ function curateScene(rawScene, assets) {
|
||||
movies: assets.movies.map((movie) => ({
|
||||
id: movie.id,
|
||||
slug: movie.slug,
|
||||
title: movie.title,
|
||||
covers: movie.movie_covers?.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index) || [],
|
||||
title: censor(movie.title, context.restriction),
|
||||
covers: movie.movie_covers && !context.restriction
|
||||
? movie.movie_covers.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index)
|
||||
: [],
|
||||
})),
|
||||
series: assets.series.map((serie) => ({
|
||||
id: serie.id,
|
||||
slug: serie.slug,
|
||||
title: serie.title,
|
||||
poster: curateMedia(serie.serie_poster, { type: 'poster' }),
|
||||
poster: context.restriction
|
||||
? null
|
||||
: (serie.serie_poster, { type: 'poster' }),
|
||||
})),
|
||||
poster: curateMedia(assets.poster, { type: 'poster' }),
|
||||
trailer: curateMedia(assets.trailer, { type: 'trailer' }),
|
||||
teaser: curateMedia(assets.teaser, { type: 'teaser' }),
|
||||
photos: assets.photos?.map((photo) => curateMedia(photo, { type: 'photo' })) || [],
|
||||
caps: assets.caps?.map((cap) => curateMedia(cap, { type: 'cap' })) || [],
|
||||
stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
|
||||
fingerprints: assets.fingerprints?.map((fingerprint) => ({
|
||||
hash: fingerprint.hash,
|
||||
type: fingerprint.type,
|
||||
duration: fingerprint.duration,
|
||||
source: fingerprint.source,
|
||||
submissions: fingerprint.source_submissions,
|
||||
createdAt: fingerprint.created_at,
|
||||
})) || [],
|
||||
createdBatchId: rawScene.created_batch_id,
|
||||
updatedBatchId: rawScene.updated_batch_id,
|
||||
isNew: assets.lastBatchId === rawScene.created_batch_id,
|
||||
};
|
||||
|
||||
curatedScene.watchUrl = getAffiliateUrl(curatedScene);
|
||||
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
|
||||
|
||||
if (!isVideoRestricted || reqUser?.abilities?.some((ability) => ability.trailerAccess)) {
|
||||
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted });
|
||||
curatedScene.teaser = curateMedia(assets.teaser, { type: 'teaser', isRestricted: isVideoRestricted });
|
||||
}
|
||||
|
||||
curatedScene.watchUrl = getAffiliateSceneUrl(curatedScene);
|
||||
|
||||
return curatedScene;
|
||||
}
|
||||
@@ -172,8 +162,9 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
caps,
|
||||
trailers,
|
||||
teasers,
|
||||
fingerprints,
|
||||
stashes,
|
||||
lastBatch: { id: lastBatchId },
|
||||
lastBatch,
|
||||
} = await promiseProps({
|
||||
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
||||
channels: knex('releases')
|
||||
@@ -189,8 +180,17 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.whereIn('releases.id', sceneIds)
|
||||
.leftJoin('entities as channels', 'channels.id', 'releases.entity_id')
|
||||
.leftJoin('entities as networks', 'networks.id', 'channels.parent_id')
|
||||
.leftJoin('affiliates', knex.raw('affiliates.entity_id in (channels.id, networks.id)'))
|
||||
.groupBy('channels.id', 'networks.id', 'affiliates.id'),
|
||||
// .leftJoin('affiliates as channel_affiliates', 'channel_affiliates.entity_id', 'channels.id')
|
||||
// .leftJoin('affiliates as network_affiliates', 'network_affiliates.entity_id', 'networks.id')
|
||||
.joinRaw(`
|
||||
left join lateral (
|
||||
select *
|
||||
from affiliates
|
||||
where affiliates.entity_id in (channels.id, networks.id)
|
||||
order by (affiliates.entity_id = channels.id) desc
|
||||
limit 1
|
||||
) affiliates ON TRUE
|
||||
`),
|
||||
studios: knex('releases')
|
||||
.whereIn('releases.id', sceneIds)
|
||||
.leftJoin('entities as studios', 'studios.id', 'releases.studio_id'),
|
||||
@@ -199,14 +199,17 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.select(
|
||||
'actors.*',
|
||||
knex.raw('row_to_json(avatars) as avatar'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||
'countries.name as birth_country_name',
|
||||
'countries.alias as birth_country_alias',
|
||||
'releases_actors.release_id',
|
||||
)
|
||||
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||
.leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2')
|
||||
.whereIn('release_id', sceneIds),
|
||||
.whereIn('release_id', sceneIds)
|
||||
.groupBy('actors.id', 'releases_actors.release_id', 'avatars.id', 'countries.name', 'countries.alias', 'sfw_media.id'),
|
||||
directors: knex('releases_directors')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||
@@ -244,17 +247,23 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.whereIn('scene_id', sceneIds)
|
||||
.groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [],
|
||||
posters: knex('releases_posters')
|
||||
.select('media.*', 'releases_posters.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
||||
photos: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'releases_posters.release_id', 'sfw_media.id'),
|
||||
photos: context.includeAssets && !context.restriction ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('releases_photos')
|
||||
.select('media.*', 'releases_photos.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('index');
|
||||
.orderBy('index')
|
||||
.groupBy('media.id', 'releases_photos.release_id', 'sfw_media.id');
|
||||
}) : [],
|
||||
caps: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -262,9 +271,12 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
}
|
||||
|
||||
return trx('releases_caps')
|
||||
.select('media.*', 'releases_caps.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('index');
|
||||
.orderBy('index')
|
||||
.groupBy('media.id', 'releases_caps.release_id', 'sfw_media.id');
|
||||
}) : [],
|
||||
trailers: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -284,6 +296,20 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_teasers.media_id');
|
||||
}) : [],
|
||||
fingerprints: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('releases_fingerprints')
|
||||
.select('scene_id', 'hash', 'type', 'duration', 'source', 'source_submissions', knex.raw('min(coalesce(source_created_at, created_at)) as created_at'))
|
||||
.whereIn('scene_id', sceneIds)
|
||||
.orderBy([
|
||||
{ column: 'source_submissions', order: 'desc' },
|
||||
{ column: knex.raw('min(coalesce(source_created_at, created_at))'), order: 'desc' },
|
||||
])
|
||||
.groupBy(['scene_id', 'hash', 'type', 'duration', 'source', 'source_submissions']);
|
||||
}) : [],
|
||||
lastBatch: knex('batches')
|
||||
.select('id')
|
||||
.where('showcased', true)
|
||||
@@ -324,6 +350,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
const sceneCaps = caps.filter((cap) => cap.release_id === sceneId);
|
||||
const sceneTrailers = trailers.find((trailer) => trailer.release_id === sceneId);
|
||||
const sceneTeasers = teasers.find((teaser) => teaser.release_id === sceneId);
|
||||
const sceneFingerprints = fingerprints.filter((fingerprint) => fingerprint.scene_id === sceneId);
|
||||
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
|
||||
const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
|
||||
|
||||
@@ -341,10 +368,11 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
caps: sceneCaps,
|
||||
trailer: sceneTrailers,
|
||||
teaser: sceneTeasers,
|
||||
fingerprints: sceneFingerprints,
|
||||
stashes: sceneStashes,
|
||||
actorStashes: sceneActorStashes,
|
||||
lastBatchId,
|
||||
});
|
||||
lastBatchId: lastBatch?.id,
|
||||
}, reqUser, context);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -477,9 +505,11 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
builder.where('scenes.is_showcased', filters.isShowcased);
|
||||
}
|
||||
|
||||
/*
|
||||
if (filters.isShowcased) {
|
||||
builder.where('scenes.date', '>', 0);
|
||||
}
|
||||
*/
|
||||
|
||||
if (options.dedupe) {
|
||||
builder.where('scenes.dupe_index', '<', 2);
|
||||
@@ -537,7 +567,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
? sqlQuery
|
||||
: sqlQuery.replace(/scenes\./g, '');
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (process.env.NODE_ENV === 'development' && argv.debug) {
|
||||
console.log(curatedSqlQuery);
|
||||
}
|
||||
|
||||
@@ -595,14 +625,18 @@ function countAggregations(buckets) {
|
||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
}
|
||||
|
||||
export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
export async function fetchScenes(filters, rawOptions, reqUser, context) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
console.log('filters', filters);
|
||||
console.log('options', options);
|
||||
if (argv.debug) {
|
||||
console.log('filters', filters);
|
||||
console.log('options', options);
|
||||
}
|
||||
|
||||
console.time('manticore sql');
|
||||
|
||||
const result = await queryManticoreSql(filters, options, reqUser);
|
||||
|
||||
console.timeEnd('manticore sql');
|
||||
|
||||
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
|
||||
@@ -615,16 +649,16 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
console.time('fetch aggregations');
|
||||
|
||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [],
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||
]);
|
||||
|
||||
console.timeEnd('fetch aggregations');
|
||||
|
||||
console.time('fetch full');
|
||||
const sceneIds = result.scenes.map((scene) => Number(scene.id));
|
||||
const scenes = await fetchScenesById(sceneIds, { reqUser });
|
||||
const scenes = await fetchScenesById(sceneIds, { reqUser, ...context });
|
||||
console.timeEnd('fetch full');
|
||||
|
||||
return {
|
||||
|
||||
303
src/stashes.js
303
src/stashes.js
@@ -35,17 +35,36 @@ export function curateStash(stash, assets = {}) {
|
||||
avatar: `/media/avatars/${assets.user.id}_${assets.user.username}.png`,
|
||||
createdAt: assets.user.created_at,
|
||||
} : null,
|
||||
comment: stash.comment,
|
||||
meta: stash.meta,
|
||||
};
|
||||
|
||||
return curatedStash;
|
||||
}
|
||||
|
||||
function curateStashed(stashed) {
|
||||
if (!stashed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedStashed = {
|
||||
id: stashed.id,
|
||||
stashId: stashed.stash_id,
|
||||
actorId: stashed.actor_id,
|
||||
createdAt: stashed.created_at,
|
||||
};
|
||||
|
||||
return curatedStashed;
|
||||
}
|
||||
|
||||
function curateStashEntry(stash, user) {
|
||||
const curatedStashEntry = {
|
||||
user_id: user?.id || undefined,
|
||||
name: stash.name || undefined,
|
||||
slug: slugify(stash.name) || undefined,
|
||||
public: stash.isPublic ?? false,
|
||||
comment: stash.comment,
|
||||
meta: stash.meta,
|
||||
};
|
||||
|
||||
return curatedStashEntry;
|
||||
@@ -57,9 +76,17 @@ function verifyStashAccess(stash, sessionUser) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchStashById(stashId, sessionUser) {
|
||||
export async function fetchStashById(stashIdOrSlug, sessionUser) {
|
||||
const stash = await knex('stashes')
|
||||
.where('id', stashId)
|
||||
.where((builder) => {
|
||||
if (typeof stashIdOrSlug === 'number') {
|
||||
builder.where('id', stashIdOrSlug);
|
||||
} else {
|
||||
builder
|
||||
.where('slug', stashIdOrSlug)
|
||||
.where('user_id', sessionUser.id);
|
||||
}
|
||||
})
|
||||
.first();
|
||||
|
||||
verifyStashAccess(stash, sessionUser);
|
||||
@@ -67,8 +94,16 @@ export async function fetchStashById(stashId, sessionUser) {
|
||||
return curateStash(stash);
|
||||
}
|
||||
|
||||
export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUser) {
|
||||
const user = await knex('users').where('username', username).first();
|
||||
export async function fetchStashByUsernameAndSlug(usernameOrId, stashSlug, sessionUser) {
|
||||
const user = await knex('users')
|
||||
.where((builder) => {
|
||||
if (typeof usernameOrId === 'number') {
|
||||
builder.where('id', usernameOrId);
|
||||
} else {
|
||||
builder.where('username', usernameOrId);
|
||||
}
|
||||
})
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
throw new HttpError('This user does not exist.', 404);
|
||||
@@ -86,10 +121,22 @@ export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUs
|
||||
return curateStash(stash, { user });
|
||||
}
|
||||
|
||||
export async function fetchUserStashes(userId, reqUser) {
|
||||
export async function fetchUserStashes(usernameOrId, reqUser) {
|
||||
const userId = typeof usernameOrId === 'number'
|
||||
? usernameOrId
|
||||
: await knex('users')
|
||||
.where('username', usernameOrId)
|
||||
.first()
|
||||
.then((user) => user?.id);
|
||||
|
||||
if (!userId) {
|
||||
throw new HttpError(`Could not find user '${usernameOrId}'`);
|
||||
}
|
||||
|
||||
const stashes = await knex('stashes')
|
||||
.select('stashes.*', 'stashes_meta.*')
|
||||
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
|
||||
.leftJoin('users', 'users.id', 'stashes.user_id')
|
||||
.where('user_id', userId)
|
||||
.modify((builder) => {
|
||||
if (userId !== reqUser?.id) {
|
||||
@@ -172,7 +219,7 @@ export async function createStash(newStash, sessionUser) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateStash(stashId, updatedStash, sessionUser) {
|
||||
export async function updateStash(stashIdOrSlug, updatedStash, sessionUser) {
|
||||
if (!sessionUser) {
|
||||
throw new HttpError('You are not authenthicated', 401);
|
||||
}
|
||||
@@ -182,11 +229,15 @@ export async function updateStash(stashId, updatedStash, sessionUser) {
|
||||
}
|
||||
|
||||
try {
|
||||
const stash = await knex('stashes')
|
||||
.where({
|
||||
id: stashId,
|
||||
user_id: sessionUser.id,
|
||||
const [stash] = await knex('stashes')
|
||||
.where((builder) => {
|
||||
if (typeof stashIdOrSlug === 'number') {
|
||||
builder.where('id', stashIdOrSlug);
|
||||
} else {
|
||||
builder.where('slug', stashIdOrSlug);
|
||||
}
|
||||
})
|
||||
.where('user_id', sessionUser.id)
|
||||
.update(curateStashEntry(updatedStash))
|
||||
.returning('*');
|
||||
|
||||
@@ -209,57 +260,86 @@ export async function removeStash(stashId, sessionUser) {
|
||||
throw new HttpError('You are not authenthicated', 401);
|
||||
}
|
||||
|
||||
const removed = await knex('stashes')
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
const [removed] = await knex('stashes')
|
||||
.where({
|
||||
id: stashId,
|
||||
id: stash.id,
|
||||
user_id: sessionUser.id,
|
||||
primary: false,
|
||||
})
|
||||
.delete();
|
||||
.delete()
|
||||
.returning('*');
|
||||
|
||||
if (removed === 0) {
|
||||
throw new HttpError('Unable to remove this stash', 400);
|
||||
if (!removed) {
|
||||
throw new HttpError('This stash could not be removed', 409);
|
||||
}
|
||||
|
||||
return curateStash(removed);
|
||||
}
|
||||
|
||||
export async function stashActor(actorId, stashId, sessionUser) {
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
const [stashed] = await knex('stashes_actors')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
actor_id: actorId,
|
||||
})
|
||||
.returning(['id', 'created_at']);
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
await indexApi.replace({
|
||||
index: 'actors_stashed',
|
||||
id: stashed.id,
|
||||
doc: {
|
||||
actor_id: actorId,
|
||||
user_id: sessionUser.id,
|
||||
stash_id: stashId,
|
||||
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const [stashed] = await knex('stashes_actors')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
actor_id: actorId,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed actor ${actorId} to stash ${stash.id} (${stash.name})`);
|
||||
await indexApi.replace({
|
||||
index: 'actors_stashed',
|
||||
id: stashed.id,
|
||||
doc: {
|
||||
actor_id: actorId,
|
||||
user_id: sessionUser.id,
|
||||
stash_id: stash.id,
|
||||
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||
},
|
||||
});
|
||||
|
||||
refreshView('actors');
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed actor ${actorId} to stash ${stash.id} (${stash.name})`);
|
||||
|
||||
return fetchDomainStashes('actor', actorId, sessionUser);
|
||||
refreshView('actors');
|
||||
|
||||
// return fetchDomainStashes('actor', actorId, sessionUser);
|
||||
return curateStashed(stashed);
|
||||
} catch (error) {
|
||||
if (error.routine === '_bt_check_unique') {
|
||||
throw new HttpError(`Actor ${actorId} is already stashed in '${stash.name}'`, 409);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unstashActor(actorId, stashId, sessionUser) {
|
||||
await knex
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
const [unstashed] = await knex
|
||||
.from('stashes_actors AS deletable')
|
||||
.where('deletable.actor_id', actorId)
|
||||
.where('deletable.stash_id', stashId)
|
||||
.where('deletable.stash_id', stash.id)
|
||||
.whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security
|
||||
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
|
||||
.where('stashes_actors.stash_id', knex.raw('deletable.stash_id'))
|
||||
.where('stashes.user_id', sessionUser.id))
|
||||
.delete();
|
||||
.delete()
|
||||
.returning('*');
|
||||
|
||||
try {
|
||||
await indexApi.callDelete({
|
||||
@@ -268,7 +348,7 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
||||
bool: {
|
||||
must: [
|
||||
{ equals: { actor_id: actorId } },
|
||||
{ equals: { stash_id: stashId } },
|
||||
{ equals: { stash_id: stash.id } },
|
||||
{ equals: { user_id: sessionUser.id } },
|
||||
],
|
||||
},
|
||||
@@ -278,52 +358,73 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId}`);
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId} (${stash.name})`);
|
||||
|
||||
refreshView('actors');
|
||||
|
||||
return fetchDomainStashes('actor', actorId, sessionUser);
|
||||
// return fetchDomainStashes('actor', actorId, sessionUser);
|
||||
return curateStashed(unstashed);
|
||||
}
|
||||
|
||||
export async function stashScene(sceneId, stashId, sessionUser) {
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
const [stashed] = await knex('stashes_scenes')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
scene_id: sceneId,
|
||||
})
|
||||
.returning(['id', 'created_at']);
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
await indexApi.replace({
|
||||
index: 'scenes_stashed',
|
||||
id: stashed.id,
|
||||
doc: {
|
||||
// ...doc.replace.doc,
|
||||
scene_id: sceneId,
|
||||
user_id: sessionUser.id,
|
||||
stash_id: stashId,
|
||||
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const [stashed] = await knex('stashes_scenes')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
scene_id: sceneId,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed scene ${sceneId} to stash ${stash.id} (${stash.name})`);
|
||||
await indexApi.replace({
|
||||
index: 'scenes_stashed',
|
||||
id: stashed.id,
|
||||
doc: {
|
||||
// ...doc.replace.doc,
|
||||
scene_id: sceneId,
|
||||
user_id: sessionUser.id,
|
||||
stash_id: stash.id,
|
||||
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||
},
|
||||
});
|
||||
|
||||
refreshView('scenes');
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed scene ${sceneId} to stash ${stash.id} (${stash.name})`);
|
||||
|
||||
return fetchDomainStashes('scene', sceneId, sessionUser);
|
||||
refreshView('scenes');
|
||||
|
||||
// return fetchDomainStashes('scene', sceneId, sessionUser);
|
||||
return curateStashed(stashed);
|
||||
} catch (error) {
|
||||
if (error.routine === '_bt_check_unique') {
|
||||
throw new HttpError(`Scene ${sceneId} is already stashed in '${stash.name}'`, 409);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||
await knex
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
const [unstashed] = await knex
|
||||
.from('stashes_scenes AS deletable')
|
||||
.where('deletable.scene_id', sceneId)
|
||||
.where('deletable.stash_id', stashId)
|
||||
.where('deletable.stash_id', stash.id)
|
||||
.whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security
|
||||
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
|
||||
.where('stashes_scenes.stash_id', knex.raw('deletable.stash_id'))
|
||||
.where('stashes.user_id', sessionUser.id))
|
||||
.delete();
|
||||
.delete()
|
||||
.returning('*');
|
||||
|
||||
await indexApi.callDelete({
|
||||
index: 'scenes_stashed',
|
||||
@@ -331,57 +432,78 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||
bool: {
|
||||
must: [
|
||||
{ equals: { scene_id: sceneId } },
|
||||
{ equals: { stash_id: stashId } },
|
||||
{ equals: { stash_id: stash.id } },
|
||||
{ equals: { user_id: sessionUser.id } },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stashId}`);
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stash.id} (${stash.name})`);
|
||||
|
||||
refreshView('scenes');
|
||||
|
||||
return fetchDomainStashes('scene', sceneId, sessionUser);
|
||||
// return fetchDomainStashes('scene', sceneId, sessionUser);
|
||||
return curateStashed(unstashed);
|
||||
}
|
||||
|
||||
export async function stashMovie(movieId, stashId, sessionUser) {
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
const [stashed] = await knex('stashes_movies')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
movie_id: movieId,
|
||||
})
|
||||
.returning(['id', 'created_at']);
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
await indexApi.replace({
|
||||
index: 'movies_stashed',
|
||||
id: stashed.id,
|
||||
doc: {
|
||||
movie_id: movieId,
|
||||
user_id: sessionUser.id,
|
||||
stash_id: stashId,
|
||||
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const [stashed] = await knex('stashes_movies')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
movie_id: movieId,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed movie ${movieId} to stash ${stash.id} (${stash.name})`);
|
||||
await indexApi.replace({
|
||||
index: 'movies_stashed',
|
||||
id: stashed.id,
|
||||
doc: {
|
||||
movie_id: movieId,
|
||||
user_id: sessionUser.id,
|
||||
stash_id: stash.id,
|
||||
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||
},
|
||||
});
|
||||
|
||||
refreshView('movies');
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed movie ${movieId} to stash ${stash.id} (${stash.name})`);
|
||||
|
||||
return fetchDomainStashes('movie', movieId, sessionUser);
|
||||
refreshView('movies');
|
||||
|
||||
// return fetchDomainStashes('movie', movieId, sessionUser);
|
||||
return curateStashed(stashed);
|
||||
} catch (error) {
|
||||
if (error.routine === '_bt_check_unique') {
|
||||
throw new HttpError(`Movie ${movieId} is already stashed in '${stash.name}'`, 409);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unstashMovie(movieId, stashId, sessionUser) {
|
||||
await knex
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
throw new HttpError(`Could not find stash '${stashId}'`, 404);
|
||||
}
|
||||
|
||||
const [unstashed] = await knex
|
||||
.from('stashes_movies AS deletable')
|
||||
.where('deletable.movie_id', movieId)
|
||||
.where('deletable.stash_id', stashId)
|
||||
.where('deletable.stash_id', stash.id)
|
||||
.whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security
|
||||
.leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id')
|
||||
.where('stashes_movies.stash_id', knex.raw('deletable.stash_id'))
|
||||
.where('stashes.user_id', sessionUser.id))
|
||||
.returning('*')
|
||||
.delete();
|
||||
|
||||
await indexApi.callDelete({
|
||||
@@ -390,18 +512,19 @@ export async function unstashMovie(movieId, stashId, sessionUser) {
|
||||
bool: {
|
||||
must: [
|
||||
{ equals: { movie_id: movieId } },
|
||||
{ equals: { stash_id: stashId } },
|
||||
{ equals: { stash_id: stash.id } },
|
||||
{ equals: { user_id: sessionUser.id } },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed movie ${movieId} from stash ${stashId}`);
|
||||
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed movie ${movieId} from stash ${stash.id} (${stash.name})`);
|
||||
|
||||
refreshView('movies');
|
||||
|
||||
return fetchDomainStashes('movie', movieId, sessionUser);
|
||||
// return fetchDomainStashes('movie', movieId, sessionUser);
|
||||
return curateStashed(unstashed);
|
||||
}
|
||||
|
||||
CronJob.from({
|
||||
|
||||
51
src/tags.js
51
src/tags.js
@@ -1,30 +1,21 @@
|
||||
import knex from './knex.js';
|
||||
import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
import { curateEntity } from './entities.js';
|
||||
import { curateMedia } from './media.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
function curateTag(tag, context) {
|
||||
function curateTag(tag, context = {}) {
|
||||
return {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
slug: tag.slug,
|
||||
description: tag.description,
|
||||
description: context.restriction ? null : tag.description, // censor interferes with markdown
|
||||
priority: tag.priority,
|
||||
poster: tag.poster && {
|
||||
id: tag.poster.id,
|
||||
path: tag.poster.path,
|
||||
thumbnail: tag.poster.thumbnail,
|
||||
lazy: tag.poster.lazy,
|
||||
isS3: tag.poster.is_s3,
|
||||
comment: tag.poster.comment,
|
||||
entity: tag.poster.entity && curateEntity({
|
||||
...tag.poster.entity,
|
||||
parent: tag.poster.entity_parent,
|
||||
}),
|
||||
},
|
||||
poster: tag.poster && curateMedia(tag.poster),
|
||||
photos: tag.photos?.map((photo) => curateMedia(photo)) || [],
|
||||
alerts: {
|
||||
only: context?.alerts?.filter((alert) => alert.is_only).flatMap((alert) => alert.alert_ids) || [],
|
||||
multi: context?.alerts?.filter((alert) => !alert.is_only).flatMap((alert) => alert.alert_ids) || [],
|
||||
@@ -33,7 +24,7 @@ function curateTag(tag, context) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchTags(options = {}) {
|
||||
export async function fetchTags(options = {}, context = {}) {
|
||||
const query = options.query?.trim();
|
||||
|
||||
const [tags, posters] = await Promise.all([
|
||||
@@ -65,10 +56,13 @@ export async function fetchTags(options = {}) {
|
||||
}
|
||||
}),
|
||||
knex('tags_posters')
|
||||
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id'),
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
|
||||
]);
|
||||
|
||||
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
|
||||
@@ -76,11 +70,11 @@ export async function fetchTags(options = {}) {
|
||||
return tags.map((tagEntry) => curateTag({
|
||||
...tagEntry,
|
||||
poster: postersByTagId[tagEntry.id],
|
||||
}));
|
||||
}, context));
|
||||
}
|
||||
|
||||
export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
const [tags, posters, alerts] = await Promise.all([
|
||||
export async function fetchTagsById(tagIds, options = {}, reqUser, context = {}) {
|
||||
const [tags, posters, photos, alerts] = await Promise.all([
|
||||
knex('tags')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string'))
|
||||
@@ -90,9 +84,20 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}
|
||||
}),
|
||||
knex('tags_posters')
|
||||
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('tags', 'tags.id', 'tags_posters.tag_id')
|
||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string'))
|
||||
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
|
||||
context.restriction ? [] : knex('tags_photos')
|
||||
.select('tags_photos.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('tags', 'tags.id', 'tags_photos.tag_id')
|
||||
.leftJoin('media', 'media.id', 'tags_photos.media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
@@ -116,9 +121,11 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
return tags.map((tagEntry) => curateTag({
|
||||
...tagEntry,
|
||||
poster: postersByTagId[tagEntry.id],
|
||||
photos: photos.filter((photo) => photo.tag_id === tagEntry.id),
|
||||
}, {
|
||||
alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id),
|
||||
append: options.append,
|
||||
...context,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -133,9 +140,11 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
return curateTag({
|
||||
...tag,
|
||||
poster: postersByTagId[tag.id],
|
||||
photos: photos.filter((photo) => photo.tag_id === tag.id),
|
||||
}, {
|
||||
alerts: alerts.filter((alert) => alert.tag_id === tag.id),
|
||||
append: options.append,
|
||||
...context,
|
||||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
|
||||
32
src/tools/slugify-test.js
Normal file
32
src/tools/slugify-test.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import slugify from '../utils/slugify.js';
|
||||
|
||||
function init() {
|
||||
const cases = [
|
||||
'Brave, New World',
|
||||
'Jœrgenbahn Straße',
|
||||
'Partêrre',
|
||||
'Ápres ski.',
|
||||
'very 😀 true 😃',
|
||||
'a véééry long piece of text that should not result in a very long slug, even for $100',
|
||||
'don\'t you, forget about me',
|
||||
'Pneumonoultramicroscopicsilicovolcanoconiosis',
|
||||
'this (old) spicemen[sic]',
|
||||
'contact@example.com',
|
||||
'!@#$%',
|
||||
'',
|
||||
' ',
|
||||
['this is', '2026-01-01', 'an array', '', ' ', 'test'],
|
||||
];
|
||||
|
||||
cases.forEach((item) => console.log(item, '-->', slugify(item, '-', { limit: 20 })));
|
||||
|
||||
cases.forEach((item) => console.log(item, '-->', slugify(item, '-', {
|
||||
lower: true,
|
||||
accents: false,
|
||||
punctuation: false,
|
||||
symbols: 'split',
|
||||
limit: 50,
|
||||
})));
|
||||
}
|
||||
|
||||
init();
|
||||
22
src/users.js
22
src/users.js
@@ -25,10 +25,11 @@ export function curateUser(user, _assets = {}) {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
emailVerified: user.email_verified,
|
||||
identityVerified: user.identity_verified,
|
||||
isEmailVerified: user.email_verified,
|
||||
isIdentityVerified: user.identity_verified,
|
||||
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
||||
role: user.role,
|
||||
abilities: [...user.role_abilities || [], ...user.abilities || []],
|
||||
createdAt: user.created_at,
|
||||
};
|
||||
|
||||
@@ -61,23 +62,6 @@ export async function fetchUser(userId, options = {}, _reqUser) {
|
||||
throw new HttpError(`User '${userId}' not found`, 404);
|
||||
}
|
||||
|
||||
/*
|
||||
const [stashes, templates] = await Promise.all([
|
||||
knex('stashes')
|
||||
.select('stashes.*', 'stashes_meta.*')
|
||||
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
|
||||
.where('user_id', user.id)
|
||||
.modify((builder) => {
|
||||
if (reqUser?.id !== user.id && !options.includeStashes) {
|
||||
builder.where('public', true);
|
||||
}
|
||||
}),
|
||||
options.includeTemplates
|
||||
? knex('users_templates').where('user_id', user.id)
|
||||
: null,
|
||||
]);
|
||||
*/
|
||||
|
||||
if (options.raw) {
|
||||
// return { user, stashes, templates };
|
||||
return { user };
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
const substitutes = {
|
||||
const accentMap = {
|
||||
à: 'a',
|
||||
á: 'a',
|
||||
ä: 'a',
|
||||
å: 'a',
|
||||
ã: 'a',
|
||||
â: 'a',
|
||||
æ: 'ae',
|
||||
ç: 'c',
|
||||
è: 'e',
|
||||
é: 'e',
|
||||
ë: 'e',
|
||||
ẽ: 'e',
|
||||
ê: 'e',
|
||||
ì: 'i',
|
||||
í: 'i',
|
||||
ï: 'i',
|
||||
ĩ: 'i',
|
||||
î: 'i',
|
||||
ǹ: 'n',
|
||||
ń: 'n',
|
||||
ñ: 'n',
|
||||
@@ -21,6 +24,7 @@ const substitutes = {
|
||||
ó: 'o',
|
||||
ö: 'o',
|
||||
õ: 'o',
|
||||
ô: 'o',
|
||||
ø: 'o',
|
||||
œ: 'oe',
|
||||
ß: 'ss',
|
||||
@@ -28,48 +32,106 @@ const substitutes = {
|
||||
ú: 'u',
|
||||
ü: 'u',
|
||||
ũ: 'u',
|
||||
û: 'u',
|
||||
ỳ: 'y',
|
||||
ý: 'y',
|
||||
ÿ: 'y',
|
||||
ỹ: 'y',
|
||||
};
|
||||
|
||||
const plainCharRegex = /[a-zA-Z0-9]/;
|
||||
const defaultPunctuationRegex = /[.,?!:;&'‘’"“”…()[\]{}<>/*—]/;
|
||||
const defaultSymbolRegex = /[@$€£#%^+=\\~]/;
|
||||
|
||||
export default function slugify(strings, delimiter = '-', {
|
||||
encode = false,
|
||||
removeAccents = true,
|
||||
removePunctuation = false,
|
||||
limit = 1000,
|
||||
lower = true,
|
||||
encode = false,
|
||||
accents: keepAccents = false,
|
||||
punctuation: keepPunctuation = 'split',
|
||||
punctuationRegex = defaultPunctuationRegex,
|
||||
symbols: keepSymbols = false,
|
||||
symbolRegex = defaultSymbolRegex,
|
||||
} = {}) {
|
||||
if (!strings || (typeof strings !== 'string' && !Array.isArray(strings))) {
|
||||
return strings;
|
||||
}
|
||||
|
||||
const slugComponents = []
|
||||
.concat(strings)
|
||||
.filter(Boolean)
|
||||
.flatMap((string) => string
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(removePunctuation && /[.,:;'"_-]/g, '')
|
||||
.match(/[A-Za-zÀ-ÖØ-öø-ÿ0-9]+/g));
|
||||
const string = [].concat(strings).join(' ');
|
||||
|
||||
if (!slugComponents) {
|
||||
const casedString = lower
|
||||
? string.toLowerCase()
|
||||
: string;
|
||||
|
||||
const normalized = casedString
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.split('')
|
||||
.map((char) => {
|
||||
if (char === ' ') {
|
||||
return char;
|
||||
}
|
||||
|
||||
const lowChar = char.toLowerCase();
|
||||
|
||||
if (accentMap[lowChar]) {
|
||||
if (keepAccents) {
|
||||
return char;
|
||||
}
|
||||
|
||||
// match original case after mapping
|
||||
if (char === lowChar) {
|
||||
return accentMap[lowChar];
|
||||
}
|
||||
|
||||
return accentMap[lowChar].toUpperCase();
|
||||
}
|
||||
|
||||
if (plainCharRegex.test(char)) {
|
||||
return char;
|
||||
}
|
||||
|
||||
if (punctuationRegex.test(char)) {
|
||||
if (keepPunctuation === 'split') {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
if (keepPunctuation) {
|
||||
return char;
|
||||
}
|
||||
}
|
||||
|
||||
if (symbolRegex.test(char)) {
|
||||
if (keepSymbols === 'split') {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
if (keepSymbols) {
|
||||
return char;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}).join('');
|
||||
|
||||
const components = normalized.trim().split(/\s+/).filter(Boolean);
|
||||
|
||||
if (components.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const slug = slugComponents.reduce((acc, component, index) => {
|
||||
const slug = components.reduce((acc, component, index) => {
|
||||
const accSlug = `${acc}${index > 0 ? delimiter : ''}${component}`;
|
||||
|
||||
if (accSlug.length < limit) {
|
||||
if (removeAccents) {
|
||||
return accSlug.replace(/[à-ÿ]/g, (match) => substitutes[match] || '');
|
||||
}
|
||||
|
||||
return accSlug;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, '');
|
||||
}).slice(0, limit); // in case first word exceeds limit
|
||||
|
||||
return encode ? encodeURI(slug) : slug;
|
||||
if (encode) {
|
||||
return encodeURI(slug);
|
||||
}
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
reviewActorRevision,
|
||||
} from '../actors.js';
|
||||
|
||||
import { fetchStashByUsernameAndSlug } from '../stashes.js';
|
||||
|
||||
export function curateActorsQuery(query) {
|
||||
return {
|
||||
query: query.q,
|
||||
@@ -49,6 +51,7 @@ export const actorsSchema = `
|
||||
extend type Query {
|
||||
actors(
|
||||
query: String
|
||||
stash: String
|
||||
limit: Int! = 30
|
||||
page: Int! = 1
|
||||
order: [String!]
|
||||
@@ -115,14 +118,37 @@ function curateGraphqlActor(actor) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchActorsGraphql(query, _req) {
|
||||
function getOrder(query) {
|
||||
if (query.order) {
|
||||
return query.order;
|
||||
}
|
||||
|
||||
if (query.query) {
|
||||
return ['results', 'desc'];
|
||||
}
|
||||
|
||||
if (query.stash) {
|
||||
return ['stashed', 'desc'];
|
||||
}
|
||||
|
||||
return ['likes', 'desc'];
|
||||
}
|
||||
|
||||
export async function fetchActorsGraphql(query, req) {
|
||||
const stash = query.stash && req.user
|
||||
? await fetchStashByUsernameAndSlug(req.user.id, query.stash, req.user)
|
||||
: null;
|
||||
|
||||
const {
|
||||
actors,
|
||||
total,
|
||||
} = await fetchActors(query, {
|
||||
} = await fetchActors({
|
||||
query: query.query,
|
||||
stashId: stash?.id,
|
||||
}, {
|
||||
limit: query.limit,
|
||||
page: query.page,
|
||||
order: query.order,
|
||||
order: getOrder(query),
|
||||
aggregateCountries: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Router from 'express-promise-router';
|
||||
|
||||
import {
|
||||
fetchAlerts,
|
||||
createAlert,
|
||||
@@ -7,24 +9,176 @@ import {
|
||||
updateNotification,
|
||||
} from '../alerts.js';
|
||||
|
||||
// actors and actorIds, stashes and stashIds are aliases for consistency
|
||||
export const alertsSchema = `
|
||||
extend type Query {
|
||||
alerts: [Alert]
|
||||
|
||||
alert(
|
||||
id: Int!
|
||||
): Alert
|
||||
|
||||
notifications(
|
||||
limit: Int = 10
|
||||
): Notifications
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createAlert(
|
||||
all: Boolean = true
|
||||
allActors: Boolean = true
|
||||
allTags: Boolean = true
|
||||
allMatches: Boolean = true
|
||||
actors: [Int!]
|
||||
actorIds: [Int!]
|
||||
tags: [String!]
|
||||
tagIds: [Int!]
|
||||
entities: [String!]
|
||||
entityIds: [Int!]
|
||||
matches: [AlertMatchInput!]
|
||||
notify: Boolean = true
|
||||
email: Boolean = false
|
||||
stashes: [Int!]
|
||||
stashIds: [Int!]
|
||||
comment: String
|
||||
meta: JSON
|
||||
): Alert
|
||||
|
||||
removeAlert(id: Int!): Alert
|
||||
|
||||
updateNotification(
|
||||
id: Int!
|
||||
seen: Boolean
|
||||
): Int!
|
||||
|
||||
updateNotifications(
|
||||
seen: Boolean
|
||||
): Int!
|
||||
}
|
||||
|
||||
type AlertAnd {
|
||||
fields: Boolean
|
||||
actors: Boolean
|
||||
tags: Boolean
|
||||
entities: Boolean
|
||||
matches: Boolean
|
||||
}
|
||||
|
||||
type AlertActor {
|
||||
id: Int
|
||||
name: String
|
||||
slug: String
|
||||
}
|
||||
|
||||
type AlertTag {
|
||||
id: Int
|
||||
name: String
|
||||
slug: String
|
||||
}
|
||||
|
||||
enum EntityType {
|
||||
network
|
||||
channel
|
||||
studio
|
||||
info
|
||||
}
|
||||
|
||||
type AlertEntity {
|
||||
id: Int
|
||||
name: String
|
||||
slug: String
|
||||
type: EntityType
|
||||
}
|
||||
|
||||
type AlertMatch {
|
||||
id: Int
|
||||
property: String
|
||||
expression: String
|
||||
}
|
||||
|
||||
input AlertMatchInput {
|
||||
property: String!
|
||||
expression: String!
|
||||
}
|
||||
|
||||
type AlertStash {
|
||||
id: Int
|
||||
name: String
|
||||
slug: String
|
||||
isPrimary: Boolean
|
||||
}
|
||||
|
||||
type Alert {
|
||||
id: Int
|
||||
notify: Boolean
|
||||
email: Boolean
|
||||
isFromPreset: Boolean
|
||||
isPrimary: Boolean
|
||||
createdAt: Date
|
||||
and: AlertAnd
|
||||
actors: [AlertActor]
|
||||
tags: [AlertTag]
|
||||
entities: [AlertEntity]
|
||||
matches: [AlertMatch]
|
||||
stashes: [AlertStash]
|
||||
comment: String
|
||||
meta: JSON
|
||||
}
|
||||
|
||||
type Notifications {
|
||||
notifications: [Notification!]!
|
||||
unseen: Int!
|
||||
}
|
||||
|
||||
type Notification {
|
||||
id: Int!
|
||||
alertId: Int
|
||||
sceneId: Int
|
||||
scene: Release
|
||||
isSeen: Boolean!
|
||||
matchedActors: [Actor!]!
|
||||
matchedTags: [Tag!]!
|
||||
matchedEntity: Entity
|
||||
createdAt: Date!
|
||||
}
|
||||
`;
|
||||
|
||||
export async function fetchAlertsApi(req, res) {
|
||||
const alerts = await fetchAlerts(req.user);
|
||||
|
||||
res.send(alerts);
|
||||
}
|
||||
|
||||
export async function fetchAlertsGraphql(query, req) {
|
||||
const alerts = await fetchAlerts(req.user);
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
export async function createAlertApi(req, res) {
|
||||
const alert = await createAlert(req.body, req.user);
|
||||
|
||||
res.send(alert);
|
||||
}
|
||||
|
||||
export async function createAlertGraphql(query, req) {
|
||||
const alert = await createAlert(query, req.user);
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
export async function removeAlertApi(req, res) {
|
||||
await Promise.all(req.params.alertId.split(',').map(async (alertId) => removeAlert(alertId, req.user)));
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
export async function removeAlertGraphql(query, req) {
|
||||
const removedAlert = await removeAlert(query.id, req.user);
|
||||
|
||||
return removedAlert;
|
||||
}
|
||||
|
||||
export async function fetchNotificationsApi(req, res) {
|
||||
const notifications = await fetchNotifications(req.user, {
|
||||
limit: req.query.limit || 10,
|
||||
@@ -33,14 +187,44 @@ export async function fetchNotificationsApi(req, res) {
|
||||
res.send(notifications);
|
||||
}
|
||||
|
||||
export async function fetchNotificationsGraphql(query, req) {
|
||||
const notifications = await fetchNotifications(req.user, {
|
||||
limit: query.limit || 10,
|
||||
});
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
export async function updateNotificationsApi(req, res) {
|
||||
await updateNotifications(req.body, req.user);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
export async function updateNotificationsGraphql(query, req) {
|
||||
const updatedCount = await updateNotifications(query, req.user);
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
export async function updateNotificationApi(req, res) {
|
||||
await updateNotification(req.params.notificationId, req.body, req.user);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
export async function updateNotificationGraphql(query, req) {
|
||||
const updatedNotification = await updateNotification(query.id, query, req.user);
|
||||
|
||||
return updatedNotification;
|
||||
}
|
||||
|
||||
export const router = Router();
|
||||
|
||||
router.get('/api/alerts', fetchAlertsApi);
|
||||
router.post('/api/alerts', createAlertApi);
|
||||
router.delete('/api/alerts/:alertId', removeAlertApi);
|
||||
|
||||
router.get('/api/users/:userId/notifications', fetchNotificationsApi);
|
||||
router.patch('/api/users/:userId/notifications', updateNotificationsApi);
|
||||
router.patch('/api/users/:userId/notifications/:notificationId', updateNotificationApi);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import argv from '../argv.js';
|
||||
|
||||
import {
|
||||
login,
|
||||
@@ -14,6 +15,10 @@ import {
|
||||
import { fetchUser } from '../users.js';
|
||||
|
||||
function getIp(req) {
|
||||
if (argv.ip) {
|
||||
return argv.ip;
|
||||
}
|
||||
|
||||
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
|
||||
|
||||
const unmappedIp = ip?.includes('.')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user