Compare commits
144 Commits
edb4be379f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b5726aec84 | |||
| 6c1f1c2a1c | |||
| 9a59448933 | |||
| c55f6d2cf2 | |||
| 16bf7b019f | |||
| 1ff5b6b036 | |||
| 3c47a1b14e | |||
| da6ccccab4 | |||
| f79ef53ebd | |||
| 185984bf0c | |||
| dd522c1fb1 | |||
| 07f290ad85 | |||
| eefc213144 | |||
| 849b4f0de7 | |||
| bf3a712de8 | |||
| 46839b48cf | |||
| f4447b23de | |||
| 8a734b9fa9 | |||
| 34ca806e84 | |||
| b7ac8917e9 | |||
| 39b25209f4 | |||
| ce287bc006 | |||
| f0e3e741ff | |||
| 856928760d | |||
| a4bd5d0d83 | |||
| 09b6db6774 | |||
| be061b956e | |||
| 074b5a4ae4 | |||
| e1e83994e0 | |||
| 9cc4b21b76 | |||
| 6634ecaa48 | |||
| b31f74ed29 | |||
| 30c5fc4c88 | |||
| 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 |
@@ -21,7 +21,7 @@
|
|||||||
"no-console": 0,
|
"no-console": 0,
|
||||||
"no-param-reassign": ["error", {
|
"no-param-reassign": ["error", {
|
||||||
"props": true,
|
"props": true,
|
||||||
"ignorePropertyModificationsFor": ["state", "acc"]
|
"ignorePropertyModificationsFor": ["state", "acc", "req"]
|
||||||
}],
|
}],
|
||||||
"vue/multi-word-component-names": 0,
|
"vue/multi-word-component-names": 0,
|
||||||
"vue/no-reserved-component-names": 0,
|
"vue/no-reserved-component-names": 0,
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"vue/html-indent": ["error", "tab"],
|
"vue/html-indent": ["error", "tab"],
|
||||||
"vue/multiline-html-element-content-newline": 0,
|
"vue/multiline-html-element-content-newline": 0,
|
||||||
"vue/no-v-html": 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": {
|
"settings": {
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -5,3 +5,5 @@ config/*
|
|||||||
log/
|
log/
|
||||||
/media
|
/media
|
||||||
data/
|
data/
|
||||||
|
assets/*.mmdb
|
||||||
|
assets/.geoipupdate.lock
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
|
|
||||||
--text: #222;
|
--text: #222;
|
||||||
--text-light: #fff;
|
--text-light: #fff;
|
||||||
|
--text-piss: #b92;
|
||||||
|
|
||||||
/* --link: #48f; */
|
/* --link: #48f; */
|
||||||
--link: var(--primary);
|
--link: var(--primary);
|
||||||
@@ -101,6 +102,7 @@
|
|||||||
--background-dim: var(--shadow-weak-10);
|
--background-dim: var(--shadow-weak-10);
|
||||||
|
|
||||||
--text: #fcfcfc;
|
--text: #fcfcfc;
|
||||||
|
--text-piss: #ba0;
|
||||||
|
|
||||||
--glass-weak-50: rgba(255, 255, 255, .02);
|
--glass-weak-50: rgba(255, 255, 255, .02);
|
||||||
--glass-weak-40: rgba(255, 255, 255, .05);
|
--glass-weak-40: rgba(255, 255, 255, .05);
|
||||||
|
|||||||
6
assets/img/icons/database-refresh.svg
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M14.422 7.078c-0.786-0.672-1.807-1.078-2.922-1.078-2.202 0-4.035 1.582-4.424 3.672h1.54c0.36-1.253 1.517-2.172 2.884-2.172 0.7 0 1.344 0.241 1.855 0.645l-1.855 1.855h4.5v-4.5l-1.578 1.578z"></path>
|
||||||
|
<path d="M11.5 13.5c-0.7 0-1.344-0.241-1.855-0.645l1.855-1.855h-4.5v4.5l1.578-1.578c0.786 0.672 1.807 1.078 2.922 1.078 2.202 0 4.035-1.582 4.424-3.672h-1.54c-0.36 1.253-1.517 2.172-2.884 2.172z"></path>
|
||||||
|
<path d="M2.158 9.146c-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062 0.169 0 0.335-0.004 0.5-0.010v-1.314c-0.165 0.008-0.332 0.012-0.5 0.012-1.152 0-2.252-0.181-3.098-0.51-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677 0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.061 0.090-0.252 0.287-0.674 0.5h1.892c0.072-0.162 0.11-0.329 0.11-0.5 0-1.381-2.462-2.5-5.5-2.5s-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.169 0 0.335-0.004 0.5-0.010v-1.312c-0.165 0.007-0.332 0.010-0.5 0.010-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.169 0 0.335-0.004 0.5-0.010v-1.312c-0.165 0.007-0.332 0.010-0.5 0.010-1.242 0-2.429-0.181-3.342-0.51z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
5
assets/img/icons/database-time.svg
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 7c3.038 0 5.5-1.119 5.5-2.5s-2.462-2.5-5.5-2.5-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.503 0 0.99-0.031 1.452-0.088-0.287-0.381-0.526-0.8-0.711-1.246-0.244 0.014-0.491 0.022-0.741 0.022-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.114 0 0.226-0.002 0.338-0.005 0.043-0.459 0.141-0.902 0.287-1.324-0.206 0.010-0.415 0.016-0.625 0.016-1.242 0-2.429-0.181-3.342-0.51-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062zM2.402 3.823c0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.078 0.115-0.367 0.402-1.074 0.677-0.846 0.329-1.946 0.51-3.098 0.51s-2.252-0.181-3.098-0.51c-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677z"></path>
|
||||||
|
<path d="M11.5 7c-2.485 0-4.5 2.015-4.5 4.5s2.015 4.5 4.5 4.5 4.5-2.015 4.5-4.5-2.015-4.5-4.5-4.5zM14 12h-3v-3h1v2h2v1z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
6
assets/img/icons/database-time2.svg
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.5 7c-2.485 0-4.5 2.015-4.5 4.5s2.015 4.5 4.5 4.5c2.485 0 4.5-2.015 4.5-4.5s-2.015-4.5-4.5-4.5zM11.5 14.688c-1.758 0-3.188-1.43-3.188-3.188s1.43-3.188 3.188-3.188 3.188 1.43 3.188 3.188-1.43 3.188-3.188 3.188z"></path>
|
||||||
|
<path d="M12 11v-2h-1v3h3v-1z"></path>
|
||||||
|
<path d="M5.5 7c3.038 0 5.5-1.119 5.5-2.5s-2.462-2.5-5.5-2.5-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.503 0 0.99-0.031 1.452-0.088-0.287-0.381-0.526-0.8-0.711-1.246-0.244 0.014-0.491 0.022-0.741 0.022-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.114 0 0.226-0.002 0.338-0.005 0.043-0.459 0.141-0.902 0.287-1.324-0.206 0.010-0.415 0.016-0.625 0.016-1.242 0-2.429-0.181-3.342-0.51-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062zM2.402 3.823c0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.078 0.115-0.367 0.402-1.074 0.677-0.846 0.329-1.946 0.51-3.098 0.51s-2.252-0.181-3.098-0.51c-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
5
assets/img/icons/loop3.svg
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.901 2.599c-1.463-1.597-3.565-2.599-5.901-2.599-4.418 0-8 3.582-8 8h1.5c0-3.59 2.91-6.5 6.5-6.5 1.922 0 3.649 0.835 4.839 2.161l-2.339 2.339h5.5v-5.5l-2.099 2.099z"></path>
|
||||||
|
<path d="M14.5 8c0 3.59-2.91 6.5-6.5 6.5-1.922 0-3.649-0.835-4.839-2.161l2.339-2.339h-5.5v5.5l2.099-2.099c1.463 1.597 3.565 2.599 5.901 2.599 4.418 0 8-3.582 8-8h-1.5z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 500 B |
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
@@ -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/img/icons/reset.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.126 9c-0.082 0.32-0.126 0.655-0.126 1 0 2.209 1.791 4 4 4s4-1.791 4-4-1.791-4-4-4v3l-5-4 5-4v3c3.314 0 6 2.686 6 6s-2.686 6-6 6-6-2.686-6-6c0-0.341 0.029-0.675 0.084-1h2.043z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 334 B |
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>
|
||||||
@@ -449,6 +449,7 @@ const showExpand = [
|
|||||||
'bust',
|
'bust',
|
||||||
'cup',
|
'cup',
|
||||||
'eyes',
|
'eyes',
|
||||||
|
'ethnicity',
|
||||||
'hairColor',
|
'hairColor',
|
||||||
'hasPiercings',
|
'hasPiercings',
|
||||||
'hasTattoos',
|
'hasTattoos',
|
||||||
@@ -628,7 +629,9 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bio-value {
|
.bio-value {
|
||||||
|
display: inline-block;
|
||||||
margin: 0 0 0 2rem;
|
margin: 0 0 0 2rem;
|
||||||
|
max-width: 20rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -851,7 +854,7 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
display: none;
|
display: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
bottom: -.25rem;
|
bottom: -.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,6 +960,10 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bio-value {
|
||||||
|
max-width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.expanded .bio-value {
|
.expanded .bio-value {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class="avatar-link no-link"
|
class="avatar-link no-link"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="actor.avatar"
|
v-if="actor.avatar && !restriction"
|
||||||
:src="getPath(actor.avatar, 'thumbnail')"
|
:src="getPath(actor.avatar, 'thumbnail')"
|
||||||
:style="{ 'background-image': `url(${getPath(actor.avatar, 'lazy')})` }"
|
:style="{ 'background-image': `url(${getPath(actor.avatar, 'lazy')})` }"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -103,7 +103,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const { user } = pageContext;
|
const { user, restriction } = pageContext;
|
||||||
const pageStash = pageContext.pageProps.stash;
|
const pageStash = pageContext.pageProps.stash;
|
||||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
>Entity Health</a>
|
>Entity Health</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
@click="initCaches"
|
||||||
|
><Icon icon="database-refresh" />Reload caches</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -37,7 +42,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import { post } from '#/src/api.js';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
async function initCaches() {
|
||||||
|
await post('/caches', null, {
|
||||||
|
successFeedback: 'Reloaded caches',
|
||||||
|
errorFeedback: 'Failed to reload caches',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -50,6 +65,7 @@ const pageContext = inject('pageContext');
|
|||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 1rem 1rem .75rem 1rem;
|
padding: 1rem 1rem .75rem 1rem;
|
||||||
border-bottom: solid 1px var(--shadow-weak-30);
|
border-bottom: solid 1px var(--shadow-weak-30);
|
||||||
margin-bottom: .25rem;
|
margin-bottom: .25rem;
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
<template>
|
<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
|
<iframe
|
||||||
v-if="campaign?.banner?.type === 'html'"
|
v-else-if="campaign?.banner?.type === 'html'"
|
||||||
ref="iframe"
|
ref="iframe"
|
||||||
:width="campaign.banner.width"
|
:width="campaign.banner.width"
|
||||||
:height="campaign.banner.height"
|
:height="campaign.banner.height"
|
||||||
@@ -18,30 +29,44 @@
|
|||||||
:href="campaign.url || campaign.affiliate?.url"
|
:href="campaign.url || campaign.affiliate?.url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="campaign"
|
class="campaign"
|
||||||
|
:style="{ 'background-image': backdrop && `url(${bannerSrc})` }"
|
||||||
|
:class="{ backdrop }"
|
||||||
data-umami-event="campaign-click"
|
data-umami-event="campaign-click"
|
||||||
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
|
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
|
||||||
>
|
>
|
||||||
<img
|
<div class="campaign-overlay">
|
||||||
:src="bannerSrc"
|
<img
|
||||||
:width="campaign.banner.width"
|
:src="bannerSrc"
|
||||||
:height="campaign.banner.height"
|
:width="campaign.banner.width"
|
||||||
class="campaign-banner"
|
:height="campaign.banner.height"
|
||||||
>
|
class="campaign-banner"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { restriction } = pageContext;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
campaign: {
|
campaign: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
backdrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(props.campaign?.banner);
|
// console.log(props.campaign);
|
||||||
|
|
||||||
const bannerSrc = (() => {
|
const bannerSrc = (() => {
|
||||||
if (props.campaign.banner) {
|
if (props.campaign.banner) {
|
||||||
|
// if (props.campaign.banner.entity.type === 'network' || props.campaign.banner.entity.isIndependent || !props.campaign.banner.entity.parent) {
|
||||||
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
|
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
|
||||||
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||||
}
|
}
|
||||||
@@ -60,7 +85,10 @@ const bannerSrc = (() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* align-items: center; */
|
border-radius: .25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame {
|
.frame {
|
||||||
@@ -68,11 +96,45 @@ const bannerSrc = (() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.campaign-overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.campaign-banner {
|
.campaign-banner {
|
||||||
width: auto;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
object-fit: contain;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,43 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul
|
<div class="tags-sections">
|
||||||
class="tags nolist"
|
<div
|
||||||
:class="{ disabled: !editing.has(item.key) }"
|
v-for="actorTags in tags"
|
||||||
>
|
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||||
<li
|
class="tags-section"
|
||||||
v-for="tag in [...item.value, ...newTags]"
|
|
||||||
:key="`tag-${tag.id}`"
|
|
||||||
class="tag"
|
|
||||||
:class="{ deleted: edits.tags && !edits.tags.some((tagId) => tagId === tag.id) }"
|
|
||||||
>
|
>
|
||||||
<span class="tag-name">{{ tag.name }}</span>
|
<ul
|
||||||
|
class="tags nolist"
|
||||||
<Icon
|
:class="{ disabled: !editing.has(item.key) }"
|
||||||
v-if="edits.tags && !edits.tags.some((tagId) => tagId === tag.id)"
|
|
||||||
icon="checkmark"
|
|
||||||
class="add"
|
|
||||||
@click="emit('tags', edits.tags.concat(tag.id))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
v-else
|
|
||||||
icon="cross2"
|
|
||||||
class="remove"
|
|
||||||
@click="emit('tags', edits.tags.filter((tagId) => tagId !== tag.id))"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="new">
|
|
||||||
<TagSearch
|
|
||||||
:disabled="!editing.has(item.key)"
|
|
||||||
@tag="addTag"
|
|
||||||
>
|
>
|
||||||
<Icon
|
<li
|
||||||
icon="plus3"
|
v-if="actorTags.actor"
|
||||||
class="add"
|
class="tags-actor"
|
||||||
/>
|
>{{ actorTags.actor.name }}:</li>
|
||||||
</TagSearch>
|
|
||||||
</li>
|
<li
|
||||||
</ul>
|
v-for="tag in [...actorTags.tags, ...newTags.filter((newTag) => newTag.actorId === actorTags.actorId)]"
|
||||||
|
:key="`tag-${tag.id}`"
|
||||||
|
class="tag"
|
||||||
|
:class="{ deleted: edits.tags && !edits.tags.some((sceneTag) => sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId) }"
|
||||||
|
>
|
||||||
|
<span class="tag-name">{{ tag.name }}</span>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="edits.tags && !edits.tags.some((sceneTag) => sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId)"
|
||||||
|
icon="checkmark"
|
||||||
|
class="add"
|
||||||
|
@click="emit('tags', edits.tags.concat(tag))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-else
|
||||||
|
icon="cross2"
|
||||||
|
class="remove"
|
||||||
|
@click="emit('tags', edits.tags.filter((sceneTag) => !(sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId)))"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="new">
|
||||||
|
<TagSearch
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@tag="(tag) => addTag(tag, actorTags.actor)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="plus3"
|
||||||
|
class="add"
|
||||||
|
/>
|
||||||
|
</TagSearch>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -70,8 +83,23 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['tags']);
|
const emit = defineEmits(['tags']);
|
||||||
|
|
||||||
function addTag(tag) {
|
const tags = [
|
||||||
if (props.edits.tags.some((tagId) => tagId === tag.id)) {
|
{
|
||||||
|
tags: props.item.value.filter((tag) => tag.actorId === null),
|
||||||
|
actor: null,
|
||||||
|
actorId: null,
|
||||||
|
},
|
||||||
|
...props.scene.actors.map((actor) => ({
|
||||||
|
tags: props.item.value.filter((tag) => tag.actorId === actor.id),
|
||||||
|
actor,
|
||||||
|
actorId: actor?.id || null,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
function addTag(newTag, actor) {
|
||||||
|
const actorId = actor?.id || null;
|
||||||
|
|
||||||
|
if (props.edits.tags.some((sceneTag) => sceneTag.id === newTag.id && sceneTag.actorId === actorId)) {
|
||||||
events.emit('feedback', {
|
events.emit('feedback', {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: 'Tag already added',
|
message: 'Tag already added',
|
||||||
@@ -80,9 +108,13 @@ function addTag(tag) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newTags.value = newTags.value.concat(tag);
|
const newTagWithActorId = {
|
||||||
|
...newTag,
|
||||||
|
actorId,
|
||||||
|
};
|
||||||
|
|
||||||
emit('tags', props.edits.tags.concat(tag.id));
|
newTags.value = newTags.value.concat(newTagWithActorId);
|
||||||
|
emit('tags', props.edits.tags.concat(newTagWithActorId));
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.scene, () => { newTags.value = []; });
|
watch(() => props.scene, () => { newTags.value = []; });
|
||||||
@@ -116,7 +148,7 @@ watch(() => props.scene, () => { newTags.value = []; });
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: .25rem;
|
margin-left: .25rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover .icon {
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +161,18 @@ watch(() => props.scene, () => { newTags.value = []; });
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .75rem;
|
||||||
|
margin: .5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-actor {
|
||||||
|
margin-right: .5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -145,9 +189,11 @@ watch(() => props.scene, () => { newTags.value = []; });
|
|||||||
.tag,
|
.tag,
|
||||||
.new {
|
.new {
|
||||||
.remove,
|
.remove,
|
||||||
.add {
|
.add,
|
||||||
|
.actor {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: .25rem .3rem;
|
padding: .25rem .3rem;
|
||||||
|
margin-left: .25rem;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
fill: var(--highlight-strong-10);
|
fill: var(--highlight-strong-10);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<span class="footer-segment">© traxxx</span>
|
<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
|
<a
|
||||||
v-if="env.links.discord"
|
v-if="env.links.discord"
|
||||||
:href="env.links.discord"
|
:href="env.links.discord"
|
||||||
@@ -56,7 +64,8 @@ const { env } = pageContext;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.discord .icon {
|
.discord .icon,
|
||||||
|
.matrix .icon {
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
fill: var(--glass-strong-10);
|
fill: var(--glass-strong-10);
|
||||||
|
|||||||
@@ -46,6 +46,13 @@
|
|||||||
@play="playing = true; paused = false;"
|
@play="playing = true; paused = false;"
|
||||||
@pause="playing = false; paused = true;"
|
@pause="playing = false; paused = true;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="(release.trailer || release.teaser).isRestricted"
|
||||||
|
v-tooltip="'Restricted video'"
|
||||||
|
icon="blocked"
|
||||||
|
class="restricted"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -71,9 +78,9 @@
|
|||||||
<div
|
<div
|
||||||
v-for="photo in [
|
v-for="photo in [
|
||||||
...(coversInAlbum ? release.covers : []),
|
...(coversInAlbum ? release.covers : []),
|
||||||
|
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
||||||
...release.photos,
|
...release.photos,
|
||||||
...release.caps,
|
...release.caps,
|
||||||
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
|
||||||
...(release.teaser?.mime.type === 'image' ? [release.poster] : []),
|
...(release.teaser?.mime.type === 'image' ? [release.poster] : []),
|
||||||
]"
|
]"
|
||||||
:key="`photo-${photo.id}`"
|
:key="`photo-${photo.id}`"
|
||||||
@@ -149,6 +156,10 @@ const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .banner {
|
||||||
|
backdrop-filter: brightness(70%) blur(1rem);
|
||||||
|
}
|
||||||
|
|
||||||
.poster-container {
|
.poster-container {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: .5rem;
|
margin-right: .5rem;
|
||||||
@@ -188,6 +199,15 @@ const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
|||||||
width: calc(21/9 * 16rem);
|
width: calc(21/9 * 16rem);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: .5rem;
|
||||||
|
fill: var(--highlight-weak-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.player) {
|
:deep(.player) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<Campaign
|
<Campaign
|
||||||
v-if="campaigns?.pagination"
|
v-if="campaigns?.pagination"
|
||||||
:campaign="campaigns.pagination"
|
:campaign="campaigns.pagination"
|
||||||
|
class="campaign-pagination"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -301,6 +302,10 @@ function getPath(page) {
|
|||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.campaign-pagination) .campaign-banner {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small-10) {
|
@media(--small-10) {
|
||||||
.campaign {
|
.campaign {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -52,6 +52,13 @@
|
|||||||
v-tooltip="'Duration'"
|
v-tooltip="'Duration'"
|
||||||
class="chapter-duration"
|
class="chapter-duration"
|
||||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
><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>
|
</span>
|
||||||
|
|
||||||
<div class="chapter-info">
|
<div class="chapter-info">
|
||||||
@@ -87,7 +94,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
import getPath from '#/src/get-path.js';
|
||||||
import { formatDuration } from '#/utils/format.js';
|
import { formatDuration, formatDate } from '#/utils/format.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
chapters: {
|
chapters: {
|
||||||
@@ -138,9 +145,9 @@ const timeline = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 .5rem;
|
padding: 0 .75rem;
|
||||||
border-radius: 0 0 .25rem .25rem;
|
border-radius: 0 0 .25rem .25rem;
|
||||||
margin: 0 0 .5rem 0;
|
margin: 0 0 .75rem 0;
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
background: var(--grey-dark-40);
|
background: var(--grey-dark-40);
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
@@ -164,7 +171,7 @@ const timeline = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chapter-info {
|
.chapter-info {
|
||||||
padding: 0 .5rem;
|
padding: 0 .75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +188,7 @@ const timeline = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chapter-description {
|
.chapter-description {
|
||||||
|
text-align: justify;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
<Campaign
|
<Campaign
|
||||||
v-if="campaigns?.meta"
|
v-if="campaigns?.meta"
|
||||||
:campaign="campaigns.meta"
|
:campaign="campaigns.meta"
|
||||||
|
class="campaign-meta"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="views">
|
<div class="views">
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
<Campaign
|
<Campaign
|
||||||
v-if="campaigns?.scope"
|
v-if="campaigns?.scope"
|
||||||
:campaign="campaigns.scope"
|
:campaign="campaigns.scope"
|
||||||
|
class="campaign-scope"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -165,7 +167,10 @@
|
|||||||
v-if="item === 'campaign' && sceneCampaign"
|
v-if="item === 'campaign' && sceneCampaign"
|
||||||
:key="`campaign-${item.id}`"
|
:key="`campaign-${item.id}`"
|
||||||
>
|
>
|
||||||
<Campaign :campaign="sceneCampaign" />
|
<Campaign
|
||||||
|
:campaign="sceneCampaign"
|
||||||
|
:backdrop="true"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
@@ -431,11 +436,11 @@ function setView(newView) {
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
padding: .5rem 1rem 1rem 1rem;
|
padding: .5rem 1rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.campaign) .campaign-banner {
|
:deep(.campaign-meta) .campaign-banner,
|
||||||
border-radius: .25rem;
|
:deep(.campaign-scope) .campaign-banner {
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
width: auto;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scopes {
|
.scopes {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
<Meta
|
<Meta
|
||||||
:scene="scene"
|
:scene="scene"
|
||||||
|
:user="user"
|
||||||
class="meta-full"
|
class="meta-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
|
|
||||||
<ul
|
<ul
|
||||||
class="row tags nolist"
|
class="row tags nolist"
|
||||||
:title="scene.tags.map((tag) => tag.name).join(', ')"
|
:title="sceneTags.map((tag) => tag.name).join(', ')"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-if="scene.shootId"
|
v-if="scene.shootId"
|
||||||
@@ -93,9 +94,10 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
v-for="tag in scene.tags"
|
v-for="tag in sceneTags"
|
||||||
:key="`tag-${scene.id}-${tag.id}`"
|
:key="`tag-${scene.id}-${tag.id}`"
|
||||||
class="tag"
|
class="tag"
|
||||||
|
:class="{ piss: tag.slug === 'pissing' }"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
:href="`/tag/${tag.slug}`"
|
:href="`/tag/${tag.slug}`"
|
||||||
@@ -129,11 +131,14 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const user = pageContext.user;
|
const { user } = pageContext;
|
||||||
const pageStash = pageContext.pageProps.stash;
|
const pageStash = pageContext.pageProps.stash;
|
||||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||||
|
|
||||||
const priorityTags = props.scene.tags.map((tag) => tag.name).slice(0, 2);
|
const tagsById = Object.fromEntries(props.scene.tags.map((tag) => [tag.id, tag]));
|
||||||
|
const sceneTags = Array.from(new Set(props.scene.tags.map((tag) => tag.id))).map((tagId) => tagsById[tagId]);
|
||||||
|
|
||||||
|
const priorityTags = sceneTags.map((tag) => tag.name).slice(0, 2);
|
||||||
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id));
|
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -262,6 +267,12 @@ const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id ===
|
|||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.tag.piss {
|
||||||
|
color: var(--text-piss);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
.shoot {
|
.shoot {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="serie-tile">
|
<div class="serie-tile">
|
||||||
<a
|
<a
|
||||||
|
v-if="serie.poster"
|
||||||
:href="`/serie/${serie.id}/${serie.slug}`"
|
:href="`/serie/${serie.id}/${serie.slug}`"
|
||||||
class="poster-container"
|
class="poster-container"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="serie.poster"
|
|
||||||
:src="getPath(serie.poster, 'thumbnail')"
|
:src="getPath(serie.poster, 'thumbnail')"
|
||||||
:style="{ 'background-image': `url(${getPath(serie.poster, 'lazy')})` }"
|
:style="{ 'background-image': `url(${getPath(serie.poster, 'lazy')})` }"
|
||||||
class="poster"
|
class="poster"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const cookies = Cookies.withConverter({
|
|||||||
const tags = {
|
const tags = {
|
||||||
anal: 'anal',
|
anal: 'anal',
|
||||||
'anal-prolapse': 'anal prolapse',
|
'anal-prolapse': 'anal prolapse',
|
||||||
|
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||||
pissing: 'pissing',
|
pissing: 'pissing',
|
||||||
gay: 'gay',
|
gay: 'gay',
|
||||||
transsexual: 'transsexual',
|
transsexual: 'transsexual',
|
||||||
|
|||||||
@@ -9,14 +9,13 @@
|
|||||||
v-for="photo in photos"
|
v-for="photo in photos"
|
||||||
:key="`photo-${photo.id}`"
|
:key="`photo-${photo.id}`"
|
||||||
:title="photo.comment"
|
:title="photo.comment"
|
||||||
:href="`/img/${photo.path}`"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="photo-container"
|
class="photo-container"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="`/${photo.thumbnail}`"
|
:src="getPath(photo, 'thumbnail', { local: true })"
|
||||||
:style="{ 'background-image': `url(/${photo.lazy})` }"
|
:style="{ 'background-image': `url(${getPath(photo, 'lazy', { local: true })})` }"
|
||||||
:alt="photo.comment"
|
:alt="photo.comment"
|
||||||
:width="photo.width"
|
:width="photo.width"
|
||||||
:height="photo.height"
|
:height="photo.height"
|
||||||
@@ -47,6 +46,8 @@ import { computed, inject } from 'vue';
|
|||||||
import Logo from '#/components/tags/logo.vue';
|
import Logo from '#/components/tags/logo.vue';
|
||||||
import Campaign from '#/components/campaigns/campaign.vue';
|
import Campaign from '#/components/campaigns/campaign.vue';
|
||||||
|
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tag: {
|
tag: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -58,11 +58,101 @@ module.exports = {
|
|||||||
address: 'http://localhost:3000/script.js',
|
address: 'http://localhost:3000/script.js',
|
||||||
siteId: '1b28ac3b-d229-43bf-aec9-75cf0a72a466',
|
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: {
|
auth: {
|
||||||
login: true,
|
login: true,
|
||||||
signup: true,
|
signup: true,
|
||||||
usernameLength: [2, 24],
|
usernameLength: [2, 24],
|
||||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
||||||
|
captcha: {
|
||||||
|
enabled: false,
|
||||||
|
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
||||||
|
secretKey: '0x0000000000000000000000000000000000000000',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bans: {
|
bans: {
|
||||||
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
||||||
@@ -84,6 +174,7 @@ module.exports = {
|
|||||||
links: {
|
links: {
|
||||||
content: 'mailto:content@traxxx.me',
|
content: 'mailto:content@traxxx.me',
|
||||||
discord: 'https://discord.gg/gY6fnq6jJV',
|
discord: 'https://discord.gg/gY6fnq6jJV',
|
||||||
|
matrix: 'https://matrix.to/#/#traxxx:matrix.unknown.name',
|
||||||
},
|
},
|
||||||
stashes: {
|
stashes: {
|
||||||
nameLength: [2, 24],
|
nameLength: [2, 24],
|
||||||
@@ -96,8 +187,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
media: {
|
media: {
|
||||||
path: './media',
|
path: './media',
|
||||||
assetPath: '/img',
|
assetPath: '',
|
||||||
mediaPath: '/media',
|
mediaPath: '/media',
|
||||||
s3Path: 'https://s3.wasabisys.com',
|
s3Path: 'https://s3.wasabisys.com',
|
||||||
|
videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ module.exports = {
|
|||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: 'traxxx',
|
name: 'traxxx',
|
||||||
script: 'npm',
|
// script: 'npm',
|
||||||
args: 'run server:prod',
|
// args: 'run server:prod',
|
||||||
|
script: './src/app.js',
|
||||||
exec_mode: 'cluster',
|
exec_mode: 'cluster',
|
||||||
instances: 2,
|
instances: 2,
|
||||||
restart_delay: 3000,
|
restart_delay: 3000,
|
||||||
|
|||||||
242
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "traxxx-web",
|
"name": "traxxx-web",
|
||||||
"version": "0.42.15",
|
"version": "0.47.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "0.42.15",
|
"version": "0.47.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@brillout/json-serializer": "^0.5.8",
|
"@brillout/json-serializer": "^0.5.8",
|
||||||
"@dicebear/collection": "^7.0.5",
|
"@dicebear/collection": "^7.0.5",
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@floating-ui/dom": "^1.5.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
|
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||||
|
"@maxmind/geoip2-node": "^6.3.4",
|
||||||
"@resvg/resvg-js": "^2.6.0",
|
"@resvg/resvg-js": "^2.6.0",
|
||||||
"@toycode/markdown-it-class": "^1.2.4",
|
"@toycode/markdown-it-class": "^1.2.4",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"cron": "^3.1.6",
|
"cron": "^3.1.6",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
|
"ejs": "^4.0.1",
|
||||||
"error-stack-parser": "^2.1.4",
|
"error-stack-parser": "^2.1.4",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-parse-resolve-info": "^4.13.0",
|
"graphql-parse-resolve-info": "^4.13.0",
|
||||||
"graphql-scalars": "^1.24.2",
|
"graphql-scalars": "^1.24.2",
|
||||||
|
"hcaptcha": "^0.2.0",
|
||||||
"ip-cidr": "^4.0.0",
|
"ip-cidr": "^4.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
@@ -50,6 +54,7 @@
|
|||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"object.omit": "^3.0.0",
|
"object.omit": "^3.0.0",
|
||||||
|
"obscenity": "^0.4.6",
|
||||||
"path-to-regexp": "^6.2.1",
|
"path-to-regexp": "^6.2.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"redis": "^4.6.12",
|
"redis": "^4.6.12",
|
||||||
@@ -3023,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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
@@ -3210,6 +3227,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@modyfi/vite-plugin-yaml": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
||||||
@@ -4513,9 +4539,10 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.5",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
@@ -5563,6 +5590,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.616",
|
"version": "1.4.616",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
||||||
@@ -6776,6 +6818,36 @@
|
|||||||
"moment": "^2.29.1"
|
"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": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -7319,6 +7391,12 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
@@ -8025,6 +8103,23 @@
|
|||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@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": {
|
"node_modules/javascript-natural-sort": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||||
@@ -8490,6 +8585,20 @@
|
|||||||
"node": ">= 18"
|
"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": {
|
"node_modules/mdurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
@@ -8633,6 +8742,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
"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": {
|
"node_modules/moment": {
|
||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
@@ -8971,6 +9090,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -10714,6 +10842,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
"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": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
||||||
@@ -13761,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": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
@@ -13892,6 +14037,14 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.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": {
|
"@modyfi/vite-plugin-yaml": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
||||||
@@ -14731,9 +14884,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"version": "3.2.5",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||||
},
|
},
|
||||||
"asynckit": {
|
"asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
@@ -15490,6 +15643,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
"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": {
|
"electron-to-chromium": {
|
||||||
"version": "1.4.616",
|
"version": "1.4.616",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
||||||
@@ -16406,6 +16567,32 @@
|
|||||||
"moment": "^2.29.1"
|
"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": {
|
"fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -16771,6 +16958,11 @@
|
|||||||
"function-bind": "^1.1.2"
|
"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": {
|
"html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
@@ -17244,6 +17436,16 @@
|
|||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@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": {
|
"javascript-natural-sort": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||||
@@ -17604,6 +17806,15 @@
|
|||||||
"typed-function": "^4.1.1"
|
"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": {
|
"mdurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
@@ -17705,6 +17916,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
"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": {
|
"moment": {
|
||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
@@ -17956,6 +18172,11 @@
|
|||||||
"es-object-atoms": "^1.0.0"
|
"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": {
|
"on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -19194,6 +19415,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
"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": {
|
"tmp": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run server:dev",
|
"dev": "node ./src/app",
|
||||||
"prod": "npm run build && npm run server:prod",
|
"prod": "npm run build && npm run server:prod",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"server:dev": "node ./src/app",
|
"server:dev": "node ./src/app",
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@floating-ui/dom": "^1.5.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
|
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||||
|
"@maxmind/geoip2-node": "^6.3.4",
|
||||||
"@resvg/resvg-js": "^2.6.0",
|
"@resvg/resvg-js": "^2.6.0",
|
||||||
"@toycode/markdown-it-class": "^1.2.4",
|
"@toycode/markdown-it-class": "^1.2.4",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"cron": "^3.1.6",
|
"cron": "^3.1.6",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"date-fns": "^3.0.0",
|
"date-fns": "^3.0.0",
|
||||||
|
"ejs": "^4.0.1",
|
||||||
"error-stack-parser": "^2.1.4",
|
"error-stack-parser": "^2.1.4",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-parse-resolve-info": "^4.13.0",
|
"graphql-parse-resolve-info": "^4.13.0",
|
||||||
"graphql-scalars": "^1.24.2",
|
"graphql-scalars": "^1.24.2",
|
||||||
|
"hcaptcha": "^0.2.0",
|
||||||
"ip-cidr": "^4.0.0",
|
"ip-cidr": "^4.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
@@ -50,6 +54,7 @@
|
|||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"object.omit": "^3.0.0",
|
"object.omit": "^3.0.0",
|
||||||
|
"obscenity": "^0.4.6",
|
||||||
"path-to-regexp": "^6.2.1",
|
"path-to-regexp": "^6.2.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"redis": "^4.6.12",
|
"redis": "^4.6.12",
|
||||||
@@ -87,7 +92,7 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "$vite"
|
"vite": "$vite"
|
||||||
},
|
},
|
||||||
"version": "0.42.15",
|
"version": "0.47.7",
|
||||||
"imports": {
|
"imports": {
|
||||||
"#/*": "./*.js"
|
"#/*": "./*.js"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ const { actor } = pageProps;
|
|||||||
|
|
||||||
const domain = routeParams.domain;
|
const domain = routeParams.domain;
|
||||||
|
|
||||||
const photos = actor.photos.filter((photo) => photo.entropy > 5.5);
|
const badCredits = ['Pierre Woodman']; // consistently horrible photos
|
||||||
|
const photos = actor.photos.filter((photo) => photo.entropy > 5.5 && !badCredits.includes(photo.credit));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -66,7 +66,16 @@
|
|||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div class="item-header">
|
<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">
|
<div class="item-actions noselect">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -443,6 +452,7 @@ const fields = computed(() => [
|
|||||||
{
|
{
|
||||||
key: 'augmentation',
|
key: 'augmentation',
|
||||||
type: 'augmentation',
|
type: 'augmentation',
|
||||||
|
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
||||||
value: {
|
value: {
|
||||||
naturalBoobs: actor.value.naturalBoobs,
|
naturalBoobs: actor.value.naturalBoobs,
|
||||||
boobsVolume: actor.value.boobsVolume,
|
boobsVolume: actor.value.boobsVolume,
|
||||||
@@ -503,6 +513,7 @@ const fields = computed(() => [
|
|||||||
{
|
{
|
||||||
key: 'piercings',
|
key: 'piercings',
|
||||||
type: 'has',
|
type: 'has',
|
||||||
|
note: 'Excludes earrings',
|
||||||
value: {
|
value: {
|
||||||
has: actor.value.hasPiercings,
|
has: actor.value.hasPiercings,
|
||||||
description: actor.value.piercings,
|
description: actor.value.piercings,
|
||||||
@@ -685,10 +696,22 @@ async function submit() {
|
|||||||
|
|
||||||
.key {
|
.key {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-note{
|
||||||
|
fill: var(--glass);
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
cursor: help;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-submit">Log in</button>
|
<button
|
||||||
|
class="button button-submit"
|
||||||
|
:disabled="submitted"
|
||||||
|
>Log in</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-if="allowSignup"
|
v-if="allowSignup"
|
||||||
@@ -94,11 +97,13 @@ const allowSignup = pageContext.env.allowSignup;
|
|||||||
const username = ref('');
|
const username = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
|
||||||
|
const submitted = ref(false);
|
||||||
const errorMsg = ref(null);
|
const errorMsg = ref(null);
|
||||||
const userInput = ref(null);
|
const userInput = ref(null);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
|
submitted.value = true;
|
||||||
errorMsg.value = null;
|
errorMsg.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -111,6 +116,8 @@ async function login() {
|
|||||||
navigate(pageContext.urlParsed.search.r ? decodeURIComponent(pageContext.urlParsed.search.r) : `/user/${loginUser.username}`, null, { redirect: true });
|
navigate(pageContext.urlParsed.search.r ? decodeURIComponent(pageContext.urlParsed.search.r) : `/user/${loginUser.username}`, null, { redirect: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMsg.value = error.message;
|
errorMsg.value = error.message;
|
||||||
|
} finally {
|
||||||
|
submitted.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<a
|
||||||
href="/login"
|
href="/login"
|
||||||
@@ -124,12 +135,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, inject } from 'vue';
|
import { ref, onMounted, inject } from 'vue';
|
||||||
|
import VueHCaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||||
|
|
||||||
import { post } from '#/src/api.js';
|
import { post } from '#/src/api.js';
|
||||||
import navigate from '#/src/navigate.js';
|
import navigate from '#/src/navigate.js';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const user = pageContext.user;
|
const { user, env } = pageContext;
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
@@ -137,28 +149,39 @@ const password = ref('');
|
|||||||
const passwordConfirm = ref('');
|
const passwordConfirm = ref('');
|
||||||
|
|
||||||
const errorMsg = ref(null);
|
const errorMsg = ref(null);
|
||||||
|
const submitted = ref(false);
|
||||||
const userInput = ref(null);
|
const userInput = ref(null);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
const captcha = ref(null);
|
||||||
|
|
||||||
async function signup() {
|
async function signup() {
|
||||||
errorMsg.value = null;
|
errorMsg.value = null;
|
||||||
|
submitted.value = true;
|
||||||
|
|
||||||
if (password.value !== passwordConfirm.value) {
|
if (password.value !== passwordConfirm.value) {
|
||||||
errorMsg.value = 'Passwords do not match';
|
errorMsg.value = 'Passwords do not match';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (env.captcha.enabled && !captcha.value) {
|
||||||
|
errorMsg.value = 'Please complete the CAPTCHA';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newUser = await post('/users', {
|
const newUser = await post('/users', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
redirect: pageContext.urlParsed.search.r,
|
redirect: pageContext.urlParsed.search.r,
|
||||||
|
captcha: captcha.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMsg.value = error.message;
|
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 {
|
.error {
|
||||||
background: var(--error);
|
background: var(--error);
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const popularNetworks = [
|
|||||||
'elegantangel',
|
'elegantangel',
|
||||||
'evilangel',
|
'evilangel',
|
||||||
'fakehub',
|
'fakehub',
|
||||||
|
'hentaied',
|
||||||
'hookuphotshot',
|
'hookuphotshot',
|
||||||
'hussiepass',
|
'hussiepass',
|
||||||
'julesjordan',
|
'julesjordan',
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { fetchEntities } from '#/src/entities.js';
|
|||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const networks = await fetchEntities(pageContext.urlParsed.search.q
|
const networks = await fetchEntities(pageContext.urlParsed.search.q
|
||||||
? { query: pageContext.urlParsed.search.q }
|
? { query: pageContext.urlParsed.search.q }
|
||||||
: { type: 'primary' });
|
: { type: 'primary' }, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
|
|||||||
@@ -43,6 +43,14 @@
|
|||||||
>{{ entity.name }}</h2>
|
>{{ entity.name }}</h2>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="user?.abilities?.some((ability) => ability.plainUrls)"
|
||||||
|
:href="entity.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="plainurl"
|
||||||
|
><Icon icon="link" /></a>
|
||||||
|
|
||||||
<Heart
|
<Heart
|
||||||
domain="entities"
|
domain="entities"
|
||||||
:item="entity"
|
:item="entity"
|
||||||
@@ -154,7 +162,7 @@ import Movies from '#/components/movies/movies.vue';
|
|||||||
import Domains from '#/components/domains/domains.vue';
|
import Domains from '#/components/domains/domains.vue';
|
||||||
import Heart from '#/components/stashes/heart.vue';
|
import Heart from '#/components/stashes/heart.vue';
|
||||||
|
|
||||||
const { pageProps, routeParams } = inject('pageContext');
|
const { pageProps, routeParams, user } = inject('pageContext');
|
||||||
const { entity } = pageProps;
|
const { entity } = pageProps;
|
||||||
|
|
||||||
const children = ref(null);
|
const children = ref(null);
|
||||||
@@ -163,27 +171,7 @@ const expanded = ref(false);
|
|||||||
const scrollable = computed(() => children.value?.scrollWidth > children.value?.clientWidth);
|
const scrollable = computed(() => children.value?.scrollWidth > children.value?.clientWidth);
|
||||||
const domain = routeParams.domain;
|
const domain = routeParams.domain;
|
||||||
|
|
||||||
const entityUrl = (() => {
|
const entityUrl = entity.affiliateUrl || entity.url || null;
|
||||||
if (!entity.url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// affiliate might be inherited, only use full URL when directly associated
|
|
||||||
if (entity.affiliate?.url && entity.affiliate.entityId === entity.id) {
|
|
||||||
return entity.affiliate.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity.affiliate?.parameters) {
|
|
||||||
const newParams = new URLSearchParams({
|
|
||||||
...Object.fromEntries(new URL(entity.url).searchParams),
|
|
||||||
...Object.fromEntries(new URLSearchParams(entity.affiliate.parameters)),
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${entity.url}?${newParams}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity.url;
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -331,6 +319,25 @@ const entityUrl = (() => {
|
|||||||
display: none;
|
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) {
|
@media(--small-20) {
|
||||||
.logo {
|
.logo {
|
||||||
padding: .5rem 1rem;
|
padding: .5rem 1rem;
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ async function fetchReleases(pageContext, entityId) {
|
|||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
}, pageContext.user);
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchScenes(await curateScenesQuery({
|
return fetchScenes(await curateScenesQuery({
|
||||||
@@ -32,7 +34,9 @@ async function fetchReleases(pageContext, entityId) {
|
|||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
}, pageContext.user);
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
@@ -47,7 +51,9 @@ export async function onBeforeRender(pageContext) {
|
|||||||
[entity],
|
[entity],
|
||||||
entityReleases,
|
entityReleases,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user),
|
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
}),
|
||||||
fetchReleases(pageContext, entityId),
|
fetchReleases(pageContext, entityId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export async function onBeforeRender(pageContext) {
|
|||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 50,
|
limit: Number(pageContext.urlParsed.search.limit) || 50,
|
||||||
dedupe: true,
|
dedupe: true,
|
||||||
}, pageContext.user);
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function getTitle(movie) {
|
|||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const [[movie], movieScenes] = await Promise.all([
|
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({
|
fetchScenes(await curateScenesQuery({
|
||||||
...pageContext.urlQuery,
|
...pageContext.urlQuery,
|
||||||
scope: 'oldest',
|
scope: 'oldest',
|
||||||
@@ -27,7 +27,9 @@ export async function onBeforeRender(pageContext) {
|
|||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
}, pageContext.user),
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<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')}`"
|
:title="scene.date ? formatDate(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${formatDate(scene.createdAt, 'y-MM-dd')}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="date nolink"
|
class="date nolink"
|
||||||
@@ -140,20 +140,30 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul
|
<div class="tags">
|
||||||
v-if="scene.tags.length > 0"
|
<div
|
||||||
class="tags nolist"
|
v-for="actorTags in tags"
|
||||||
>
|
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||||
<li
|
class="tags-section"
|
||||||
v-for="tag in scene.tags"
|
|
||||||
:key="`tag-${tag.id}`"
|
|
||||||
>
|
>
|
||||||
<Link
|
<ul class="nolist">
|
||||||
:href="`/tag/${tag.slug}`"
|
<li
|
||||||
class="tag nolink"
|
v-if="actorTags.actor"
|
||||||
>{{ tag.name }}</Link>
|
class="tags-actor"
|
||||||
</li>
|
>{{ actorTags.actor.name }}:</li>
|
||||||
</ul>
|
|
||||||
|
<li
|
||||||
|
v-for="tag in actorTags.tags"
|
||||||
|
:key="`tag-${tag.id}`"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
:href="`/tag/${tag.slug}`"
|
||||||
|
class="tag nolink"
|
||||||
|
>{{ tag.name }}</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="scene.movies.length > 0 || scene.series.length > 0"
|
v-if="scene.movies.length > 0 || scene.series.length > 0"
|
||||||
@@ -249,12 +259,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Chapters
|
|
||||||
v-if="scene.chapters.length > 0"
|
|
||||||
:chapters="scene.chapters"
|
|
||||||
class="section"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="scene.description"
|
v-if="scene.description"
|
||||||
class="section"
|
class="section"
|
||||||
@@ -264,6 +268,18 @@
|
|||||||
<p class="description">{{ scene.description }}</p>
|
<p class="description">{{ scene.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
v-if="scene.chapters.length > 0"
|
||||||
|
class="section"
|
||||||
|
>
|
||||||
|
<h3 class="heading">Chapters</h3>
|
||||||
|
|
||||||
|
<Chapters
|
||||||
|
:chapters="scene.chapters"
|
||||||
|
class="section"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="campaigns?.scene"
|
v-if="campaigns?.scene"
|
||||||
class="section"
|
class="section"
|
||||||
@@ -343,6 +359,43 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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
|
<div
|
||||||
class="scene-actions section"
|
class="scene-actions section"
|
||||||
>
|
>
|
||||||
@@ -397,6 +450,17 @@ const {
|
|||||||
|
|
||||||
const { scene } = pageProps;
|
const { scene } = pageProps;
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
{
|
||||||
|
tags: scene.tags.filter((tag) => tag.actorId === null),
|
||||||
|
actor: null,
|
||||||
|
},
|
||||||
|
...scene.actors.map((actor) => ({
|
||||||
|
actor,
|
||||||
|
tags: scene.tags.filter((tag) => tag.actorId === actor.id),
|
||||||
|
})),
|
||||||
|
].filter((actorTags) => actorTags.tags.length > 0);
|
||||||
|
|
||||||
const showSummaryDialog = ref(false);
|
const showSummaryDialog = ref(false);
|
||||||
|
|
||||||
const qualities = {
|
const qualities = {
|
||||||
@@ -597,6 +661,22 @@ function copySummary() {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-section {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-actor {
|
||||||
|
margin-right: .5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.actors {
|
.actors {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -740,6 +820,55 @@ function copySummary() {
|
|||||||
display: none;
|
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) {
|
@media(--compact) {
|
||||||
.content {
|
.content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export async function onBeforeRender(pageContext) {
|
|||||||
includeAssets: true,
|
includeAssets: true,
|
||||||
includePartOf: true,
|
includePartOf: true,
|
||||||
actorStashes: true,
|
actorStashes: true,
|
||||||
|
restriction: pageContext.restriction,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [campaigns, tagIds] = await Promise.all([
|
const [campaigns, tagIds] = await Promise.all([
|
||||||
|
|||||||
@@ -66,7 +66,16 @@
|
|||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div class="item-header">
|
<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">
|
<div class="item-actions">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -252,6 +261,8 @@ const fields = computed(() => [
|
|||||||
key: 'tags',
|
key: 'tags',
|
||||||
type: 'tags',
|
type: 'tags',
|
||||||
value: scene.value.tags.toSorted((tagA, tagB) => tagA.name.localeCompare(tagB.name)),
|
value: scene.value.tags.toSorted((tagA, tagB) => tagA.name.localeCompare(tagB.name)),
|
||||||
|
simplify: false,
|
||||||
|
note: 'Actor-specific tags should only be used where confusion is reasonable, such as group scenes in which some perform anal, and some don\'t.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'movies',
|
key: 'movies',
|
||||||
@@ -262,11 +273,13 @@ const fields = computed(() => [
|
|||||||
key: 'title',
|
key: 'title',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: scene.value.title,
|
value: scene.value.title,
|
||||||
|
note: 'Do not correct language errors unless source was updated.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'description',
|
key: 'description',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: scene.value.description,
|
value: scene.value.description,
|
||||||
|
note: 'Do not correct language errors unless source was updated.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'date',
|
key: 'date',
|
||||||
@@ -408,6 +421,8 @@ async function submit() {
|
|||||||
|
|
||||||
.key {
|
.key {
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -473,6 +488,16 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-note{
|
||||||
|
fill: var(--glass);
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
cursor: help;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.editor-footer {
|
.editor-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export async function onBeforeRender(pageContext) {
|
|||||||
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
dedupe: true,
|
dedupe: true,
|
||||||
}, pageContext.user),
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
}),
|
||||||
getRandomCampaigns([
|
getRandomCampaigns([
|
||||||
{ minRatio: 0.75, maxRatio: 1.25 },
|
{ minRatio: 0.75, maxRatio: 1.25 },
|
||||||
{ minRatio: 1.5 },
|
{ minRatio: 1.5 },
|
||||||
|
|||||||
@@ -16,22 +16,24 @@ export async function onBeforeRender(pageContext) {
|
|||||||
}), {
|
}), {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 15,
|
limit: Number(pageContext.urlParsed.search.limit) || 15,
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchActors(curateActorsQuery(pageContext.urlQuery), {
|
fetchActors(curateActorsQuery(pageContext.urlQuery), {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 10,
|
limit: Number(pageContext.urlParsed.search.limit) || 10,
|
||||||
order: ['results', 'desc'],
|
order: ['results', 'desc'],
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchMovies(await curateMoviesQuery({
|
fetchMovies(await curateMoviesQuery({
|
||||||
...pageContext.urlQuery,
|
...pageContext.urlQuery,
|
||||||
scope: pageContext.routeParams.scope || 'results',
|
scope: pageContext.routeParams.scope || 'results',
|
||||||
}), {
|
}), {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 5,
|
limit: Number(pageContext.urlParsed.search.limit) || 5,
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchEntities({
|
fetchEntities({
|
||||||
query: pageContext.urlParsed.search.q,
|
query: pageContext.urlParsed.search.q,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
|
}, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -103,9 +103,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<time
|
<time
|
||||||
:datetime="serie.date.toISOString()"
|
:datetime="serie.effectiveDate.toISOString()"
|
||||||
class="date ellipsis"
|
class="date ellipsis compact-hide"
|
||||||
>{{ formatDate(serie.date, 'MMMM d, y') }}</time>
|
: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>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -408,6 +421,11 @@ const scenes = pageContext.pageProps.scenes;
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodate {
|
||||||
|
color: var(--highlight);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.info,
|
.info,
|
||||||
.header {
|
.header {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -537,6 +555,10 @@ const scenes = pageContext.pageProps.scenes;
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-show {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small) {
|
@media(--small) {
|
||||||
.content {
|
.content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -621,5 +643,13 @@ const scenes = pageContext.pageProps.scenes;
|
|||||||
.actors {
|
.actors {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="tag.poster"
|
v-if="tag.poster"
|
||||||
:src="`/${tag.poster.thumbnail}`"
|
:src="getPath(tag.poster, 'thumbnail', { local: true })"
|
||||||
:style="{ 'background-image': `url(/${tag.poster.lazy})` }"
|
:style="{ 'background-image': `url(${getPath(tag.poster, 'lazy', { local: true })})` }"
|
||||||
:title="tag.poster.comment"
|
:title="tag.poster.comment"
|
||||||
class="thumb"
|
class="thumb"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -111,6 +111,7 @@ import { ref, onMounted, inject } from 'vue';
|
|||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
import navigate from '#/src/navigate.js';
|
||||||
import events from '#/src/events.js';
|
import events from '#/src/events.js';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
import Logo from '#/components/tags/logo.vue';
|
import Logo from '#/components/tags/logo.vue';
|
||||||
|
|
||||||
@@ -324,6 +325,7 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
aspect-ratio: 5/3;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fetchTags, fetchTagsById } from '#/src/tags.js';
|
import { fetchTags, fetchTagsById } from '#/src/tags.js';
|
||||||
|
import { censor } from '#/src/censor.js';
|
||||||
|
|
||||||
const tagSlugs = {
|
const tagSlugs = {
|
||||||
popular: [
|
popular: [
|
||||||
@@ -117,7 +118,7 @@ const tagSlugs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function searchTags(pageContext) {
|
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 {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
@@ -136,13 +137,13 @@ export async function onBeforeRender(pageContext) {
|
|||||||
return searchTags(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 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 tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag]));
|
||||||
|
|
||||||
const tagShowcase = Object.fromEntries(Object.entries(tagSlugs).map(([category, categorySlugs]) => [
|
const tagShowcase = Object.fromEntries(Object.entries(tagSlugs).map(([category, categorySlugs]) => [
|
||||||
category,
|
censor(category, pageContext.restriction),
|
||||||
categorySlugs.map((slug) => tagsBySlug[slug]).filter(Boolean),
|
categorySlugs.map((slug) => tagsBySlug[slug]).filter(Boolean),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ async function fetchReleases(pageContext) {
|
|||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
}, pageContext.user);
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchScenes(await curateScenesQuery({
|
return fetchScenes(await curateScenesQuery({
|
||||||
@@ -33,14 +35,16 @@ async function fetchReleases(pageContext) {
|
|||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
}, pageContext.user);
|
}, pageContext.user, {
|
||||||
|
restriction: pageContext.restriction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const tagSlug = pageContext.routeParams.tagSlug;
|
const tagSlug = pageContext.routeParams.tagSlug;
|
||||||
|
|
||||||
const [[tag], tagReleases, campaigns] = await Promise.all([
|
const [[tag], tagReleases, campaigns] = await Promise.all([
|
||||||
fetchTagsById([tagSlug], {}, pageContext.user),
|
fetchTagsById([tagSlug], {}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
fetchReleases(pageContext),
|
fetchReleases(pageContext),
|
||||||
getRandomCampaigns([
|
getRandomCampaigns([
|
||||||
{ tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 },
|
{ tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 },
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export async function onBeforeRender(pageContext) {
|
|||||||
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
||||||
aggregate: withQuery,
|
aggregate: withQuery,
|
||||||
dedupe: true,
|
dedupe: true,
|
||||||
}, pageContext.user),
|
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||||
getRandomCampaigns([
|
getRandomCampaigns([
|
||||||
{ minRatio: 2.0, maxRatio: 5 },
|
{ minRatio: 2.0, maxRatio: 5 },
|
||||||
{ minRatio: 0.75, maxRatio: 1.25 },
|
{ minRatio: 0.75, maxRatio: 1.25 },
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export default {
|
|||||||
'assets',
|
'assets',
|
||||||
'campaigns',
|
'campaigns',
|
||||||
'meta',
|
'meta',
|
||||||
|
'restriction',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -122,7 +122,10 @@ export function curateActor(actor, context = {}) {
|
|||||||
state: actor.residence_state,
|
state: actor.residence_state,
|
||||||
},
|
},
|
||||||
agency: actor.agency,
|
agency: actor.agency,
|
||||||
avatar: curateMedia(actor.avatar),
|
avatar: actor.avatar && curateMedia({
|
||||||
|
...actor.avatar,
|
||||||
|
sfw_media: actor.sfw_avatar,
|
||||||
|
}),
|
||||||
socials: context.socials?.map((social) => ({
|
socials: context.socials?.map((social) => ({
|
||||||
id: social.id,
|
id: social.id,
|
||||||
url: social.url,
|
url: social.url,
|
||||||
@@ -205,26 +208,33 @@ export function sortActorsByGender(actors, context = {}) {
|
|||||||
export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||||
const [actors, profiles, photos, socials, stashes, alerts] = await Promise.all([
|
const [actors, profiles, photos, socials, stashes, alerts] = await Promise.all([
|
||||||
knex('actors')
|
knex('actors')
|
||||||
.select(
|
.select('actors.*')
|
||||||
'actors.*',
|
|
||||||
'actors_meta.stashed',
|
|
||||||
knex.raw('row_to_json(avatars) as avatar'),
|
|
||||||
'birth_countries.alpha2 as birth_country_alpha2',
|
|
||||||
knex.raw('COALESCE(birth_countries.alias, birth_countries.name) as birth_country_name'),
|
|
||||||
'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'),
|
|
||||||
)
|
|
||||||
.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('entities', 'entities.id', 'actors.entity_id')
|
|
||||||
.whereIn('actors.id', actorIds)
|
.whereIn('actors.id', actorIds)
|
||||||
.modify((builder) => {
|
.modify((builder) => {
|
||||||
if (options.order) {
|
if (options.order) {
|
||||||
builder.orderBy(...options.order);
|
builder.orderBy(...options.order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options.shallow) {
|
||||||
|
builder
|
||||||
|
.select(
|
||||||
|
'actors_meta.stashed',
|
||||||
|
knex.raw('row_to_json(avatars) as avatar'),
|
||||||
|
'birth_countries.alpha2 as birth_country_alpha2',
|
||||||
|
knex.raw('COALESCE(birth_countries.alias, birth_countries.name) as birth_country_name'),
|
||||||
|
'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')
|
||||||
|
.groupBy('actors.id', 'avatars.id', 'sfw_media.id', 'entities.id', 'actors_meta.stashed', 'birth_countries.alpha2', 'residence_countries.alpha2');
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
knex('actors_profiles')
|
knex('actors_profiles')
|
||||||
.select(
|
.select(
|
||||||
@@ -245,10 +255,12 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
|||||||
'media.*',
|
'media.*',
|
||||||
'actors_avatars.actor_id',
|
'actors_avatars.actor_id',
|
||||||
knex.raw('json_agg(actors_avatars.profile_id) as profile_ids'),
|
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)
|
.whereIn('actor_id', actorIds)
|
||||||
.leftJoin('media', 'media.id', 'actors_avatars.media_id')
|
.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'),
|
.orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'),
|
||||||
knex('actors_socials')
|
knex('actors_socials')
|
||||||
.whereIn('actor_id', actorIds),
|
.whereIn('actor_id', actorIds),
|
||||||
@@ -263,12 +275,15 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
|||||||
.where('user_id', reqUser.id)
|
.where('user_id', reqUser.id)
|
||||||
.whereIn('actor_id', actorIds)
|
.whereIn('actor_id', actorIds)
|
||||||
: [],
|
: [],
|
||||||
]);
|
].slice(0, options.shallow ? 1 : -1));
|
||||||
|
|
||||||
if (options.order) {
|
if (options.order) {
|
||||||
return actors.map((actorEntry) => curateActor(actorEntry, {
|
return actors.map((actorEntry) => curateActor(actorEntry, {
|
||||||
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
|
stashes: stashes?.filter((stash) => stash.actor_id === actorEntry.id),
|
||||||
alerts: alerts.filter((alert) => alert.actor_id === actorEntry.id),
|
alerts: alerts?.filter((alert) => alert.actor_id === actorEntry.id),
|
||||||
|
profiles: profiles?.filter((profile) => profile.actor_id === actorEntry.id),
|
||||||
|
photos: photos?.filter((photo) => photo.actor_id === actorEntry.id),
|
||||||
|
socials: socials?.filter((social) => social.actor_id === actorEntry.id),
|
||||||
append: options.append,
|
append: options.append,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -282,11 +297,11 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return curateActor(actor, {
|
return curateActor(actor, {
|
||||||
stashes: stashes.filter((stash) => stash.actor_id === actor.id),
|
stashes: stashes?.filter((stash) => stash.actor_id === actor.id),
|
||||||
alerts: alerts.filter((alert) => alert.actor_id === actor.id),
|
alerts: alerts?.filter((alert) => alert.actor_id === actor.id),
|
||||||
profiles: profiles.filter((profile) => profile.actor_id === actor.id),
|
profiles: profiles?.filter((profile) => profile.actor_id === actor.id),
|
||||||
photos: photos.filter((photo) => photo.actor_id === actor.id),
|
photos: photos?.filter((photo) => photo.actor_id === actor.id),
|
||||||
socials: socials.filter((social) => social.actor_id === actor.id),
|
socials: socials?.filter((social) => social.actor_id === actor.id),
|
||||||
append: options.append,
|
append: options.append,
|
||||||
});
|
});
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|||||||
152
src/affiliates.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
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.channelScenes === false && scene.channel && scene.affiliate.entityId !== scene.channel.id) {
|
||||||
|
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|tour)/, '')}${search}`; // replace needed for Jules Jordan and HussiePass, 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;
|
||||||
|
}
|
||||||
10
src/app.js
@@ -1,14 +1,8 @@
|
|||||||
import initServer from './web/server.js';
|
import initServer from './web/server.js';
|
||||||
import { cacheTagIds } from './tags.js';
|
import { initCaches } from './cache.js';
|
||||||
import { cacheEntityIds } from './entities.js';
|
|
||||||
import { cacheCampaigns } from './campaigns.js';
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await Promise.all([
|
await initCaches();
|
||||||
cacheTagIds(),
|
|
||||||
cacheEntityIds(),
|
|
||||||
cacheCampaigns(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
initServer();
|
initServer();
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/argv.js
@@ -1,11 +1,15 @@
|
|||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
|
||||||
const { argv } = yargs()
|
const { argv } = yargs(process.argv.slice(2))
|
||||||
.command('npm start')
|
|
||||||
.option('debug', {
|
.option('debug', {
|
||||||
describe: 'Show error stack traces',
|
describe: 'Show error stack traces and inputs',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: process.env.NODE_ENV === 'development',
|
default: process.env.NODE_ENV === 'development',
|
||||||
|
})
|
||||||
|
.option('ip', {
|
||||||
|
describe: 'Mock IP address',
|
||||||
|
type: 'string',
|
||||||
|
default: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default argv;
|
export default argv;
|
||||||
|
|||||||
12
src/auth.js
@@ -5,6 +5,7 @@ import fs from 'fs/promises';
|
|||||||
import { createAvatar } from '@dicebear/core';
|
import { createAvatar } from '@dicebear/core';
|
||||||
import { shapes } from '@dicebear/collection';
|
import { shapes } from '@dicebear/collection';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { verify } from 'hcaptcha';
|
||||||
|
|
||||||
import { knexOwner as knex } from './knex.js';
|
import { knexOwner as knex } from './knex.js';
|
||||||
import redis from './redis.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);
|
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')
|
const existingUser = await knex('users')
|
||||||
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
||||||
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
||||||
@@ -134,7 +144,7 @@ export async function signup(credentials, userIp) {
|
|||||||
primary: true,
|
primary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.verbose(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
logger.info(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
||||||
|
|
||||||
await generateAvatar({
|
await generateAvatar({
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|||||||
12
src/cache.js
@@ -1,5 +1,9 @@
|
|||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
|
|
||||||
|
import { cacheTagIds } from './tags.js';
|
||||||
|
import { cacheEntityIds } from './entities.js';
|
||||||
|
import { cacheCampaigns } from './campaigns.js';
|
||||||
|
|
||||||
export async function getIdsBySlug(slugs, domain, toMap) {
|
export async function getIdsBySlug(slugs, domain, toMap) {
|
||||||
if (!slugs) {
|
if (!slugs) {
|
||||||
return [];
|
return [];
|
||||||
@@ -25,3 +29,11 @@ export async function getIdsBySlug(slugs, domain, toMap) {
|
|||||||
|
|
||||||
return ids.filter(Boolean);
|
return ids.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initCaches() {
|
||||||
|
await Promise.all([
|
||||||
|
cacheTagIds(),
|
||||||
|
cacheEntityIds(),
|
||||||
|
cacheCampaigns(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,18 +4,41 @@ import { knexOwner as knex } from './knex.js';
|
|||||||
import { curateEntity } from './entities.js';
|
import { curateEntity } from './entities.js';
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
import initLogger from './logger.js';
|
import initLogger from './logger.js';
|
||||||
|
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
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) {
|
function curateCampaign(campaign) {
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const entity = campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity });
|
||||||
|
|
||||||
|
const curatedCampaign = {
|
||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
url: campaign.url,
|
entity,
|
||||||
entity: campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity }),
|
|
||||||
banner: campaign.banner && {
|
banner: campaign.banner && {
|
||||||
id: campaign.banner.id,
|
id: campaign.banner.id,
|
||||||
type: campaign.banner.type,
|
type: campaign.banner.type,
|
||||||
@@ -31,6 +54,10 @@ function curateCampaign(campaign) {
|
|||||||
parameters: campaign.affiliate.parameters,
|
parameters: campaign.affiliate.parameters,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
curatedCampaign.url = getCampaignUrl(campaign, entity);
|
||||||
|
|
||||||
|
return curatedCampaign;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns) {
|
function selectRandomCampaign(primaryCampaigns, entityCampaigns, preferredCampaigns) {
|
||||||
@@ -55,18 +82,27 @@ export async function getRandomCampaign(options = {}, context = {}, pass = 0) {
|
|||||||
|
|
||||||
const validCampaigns = campaigns.filter((campaign) => {
|
const validCampaigns = campaigns.filter((campaign) => {
|
||||||
if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
|
if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
|
||||||
|
// too small
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
|
if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
|
||||||
|
// too big
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
|
if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
|
||||||
|
// wrong tag
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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,34 +2,42 @@ import knex from './knex.js';
|
|||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
import initLogger from './logger.js';
|
import initLogger from './logger.js';
|
||||||
import entityPrefixes from './entities-prefixes.js';
|
import entityPrefixes from './entities-prefixes.js';
|
||||||
|
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||||
|
import { censor } from './censor.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
|
|
||||||
export function curateEntity(entity, context) {
|
export function curateEntity(entity, context = {}) {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const curatedEntity = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
name: entity.name,
|
name: censor(entity.name, context.restriction),
|
||||||
slug: entity.slug,
|
slug: entity.slug,
|
||||||
type: entity.type,
|
type: entity.type,
|
||||||
url: entity.url,
|
url: entity.url,
|
||||||
isIndependent: entity.independent,
|
isIndependent: entity.independent,
|
||||||
hasLogo: entity.has_logo,
|
hasLogo: context.restriction ? false : entity.has_logo,
|
||||||
parent: curateEntity(entity.parent, context),
|
parent: curateEntity(entity.parent, context),
|
||||||
tags: context?.tags?.map((tag) => ({
|
tags: context?.tags?.map((tag) => ({
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
slug: tag.slug,
|
slug: tag.slug,
|
||||||
})),
|
})),
|
||||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({ ...child, parent: entity }, { parent: entity })) || [],
|
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({
|
||||||
|
...child,
|
||||||
|
parent: entity,
|
||||||
|
}, {
|
||||||
|
parent: entity,
|
||||||
|
restriction: context.restriction,
|
||||||
|
})) || [],
|
||||||
affiliate: entity.affiliate ? {
|
affiliate: entity.affiliate ? {
|
||||||
id: entity.affiliate.id,
|
id: entity.affiliate.id,
|
||||||
entityId: entity.affiliate.entity_id,
|
entityId: entity.affiliate.entity_id,
|
||||||
url: entity.affiliate.url,
|
url: entity.affiliate.url,
|
||||||
parameters: entity.affiliate.parameters,
|
parameters: entity.affiliate.parameters || {},
|
||||||
} : null,
|
} : null,
|
||||||
...context?.append?.[entity.id],
|
...context?.append?.[entity.id],
|
||||||
alerts: {
|
alerts: {
|
||||||
@@ -37,9 +45,13 @@ export function curateEntity(entity, context) {
|
|||||||
multi: 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) || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
curatedEntity.affiliateUrl = getAffiliateEntityUrl(curatedEntity);
|
||||||
|
|
||||||
|
return curatedEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEntities(options = {}) {
|
export async function fetchEntities(options = {}, context = {}) {
|
||||||
const entities = await knex('entities')
|
const entities = await knex('entities')
|
||||||
.select('entities.*', knex.raw('row_to_json(parents) as parent'))
|
.select('entities.*', knex.raw('row_to_json(parents) as parent'))
|
||||||
.modify((builder) => {
|
.modify((builder) => {
|
||||||
@@ -88,11 +100,12 @@ export async function fetchEntities(options = {}) {
|
|||||||
.whereIn('entity_id', entities.map((entity) => entity.id));
|
.whereIn('entity_id', entities.map((entity) => entity.id));
|
||||||
|
|
||||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||||
|
...context,
|
||||||
tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id),
|
tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
export async function fetchEntitiesById(entityIds, options = {}, reqUser, context) {
|
||||||
const [entities, children, tags, alerts] = await Promise.all([
|
const [entities, children, tags, alerts] = await Promise.all([
|
||||||
knex('entities')
|
knex('entities')
|
||||||
.select(
|
.select(
|
||||||
@@ -113,7 +126,11 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
|||||||
options.includeChildren ? knex('entities')
|
options.includeChildren ? knex('entities')
|
||||||
.whereIn('entities.parent_id', entityIds)
|
.whereIn('entities.parent_id', entityIds)
|
||||||
.whereNot('type', 'info')
|
.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')
|
knex('entities_tags')
|
||||||
.select('entity_id', 'tags.*')
|
.select('entity_id', 'tags.*')
|
||||||
.leftJoin('tags', 'tags.id', 'tag_id')
|
.leftJoin('tags', 'tags.id', 'tag_id')
|
||||||
@@ -127,6 +144,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
|||||||
|
|
||||||
if (options.order) {
|
if (options.order) {
|
||||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||||
|
...context,
|
||||||
append: options.append,
|
append: options.append,
|
||||||
children: children.filter((channel) => channel.parent_id === entityEntry.id),
|
children: children.filter((channel) => channel.parent_id === entityEntry.id),
|
||||||
alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id),
|
alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id),
|
||||||
@@ -142,6 +160,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return curateEntity(entity, {
|
return curateEntity(entity, {
|
||||||
|
...context,
|
||||||
append: options.append,
|
append: options.append,
|
||||||
children: children.filter((channel) => channel.parent_id === entity.id),
|
children: children.filter((channel) => channel.parent_id === entity.id),
|
||||||
tags: tags.filter((tag) => tag.entity_id === entity.id),
|
tags: tags.filter((tag) => tag.entity_id === entity.id),
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
// import config from 'config';
|
|
||||||
import { pageContext } from '../renderer/usePageContext.js';
|
import { pageContext } from '../renderer/usePageContext.js';
|
||||||
|
|
||||||
function getBasePath(media, type, options) {
|
function getBasePath(media, options) {
|
||||||
/*
|
if (pageContext.restriction) {
|
||||||
if (store.state.ui.sfw) {
|
return pageContext.env.media.assetPath;
|
||||||
return config.media.assetPath;
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
if (media.isS3) {
|
if (media.isS3) {
|
||||||
return options.s3Path;
|
return options.s3Path;
|
||||||
@@ -20,15 +17,13 @@ function getBasePath(media, type, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFilename(media, type, options) {
|
function getFilename(media, type, options) {
|
||||||
/*
|
if (pageContext.restriction && type && !options?.original) {
|
||||||
if (store.state.ui.sfw && type && !options?.original) {
|
return media.sfw?.[type];
|
||||||
return media.sfw[type];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.state.ui.sfw) {
|
if (pageContext.restriction) {
|
||||||
return media.sfw.path;
|
return media.sfw?.path;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
if (type && !options?.original) {
|
if (type && !options?.original) {
|
||||||
return media[type];
|
return media[type];
|
||||||
@@ -42,7 +37,7 @@ export default function getPath(media, type, options) {
|
|||||||
return null;
|
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 });
|
const filename = getFilename(media, type, { ...pageContext.env.media, ...options });
|
||||||
|
|
||||||
return `${path}/${filename}`;
|
return `${path}/${filename}`;
|
||||||
|
|||||||
11
src/knex.js
@@ -1,15 +1,6 @@
|
|||||||
import config from 'config';
|
import config from 'config';
|
||||||
import knex from 'knex';
|
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({
|
export const knexOwner = knex({
|
||||||
client: 'pg',
|
client: 'pg',
|
||||||
connection: config.database.owner,
|
connection: config.database.owner,
|
||||||
@@ -19,6 +10,8 @@ export const knexOwner = knex({
|
|||||||
// debug: process.env.NODE_ENV === 'development',
|
// debug: process.env.NODE_ENV === 'development',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const knexQuery = knexOwner; // legacy
|
||||||
|
|
||||||
export const knexManticore = knex({
|
export const knexManticore = knex({
|
||||||
client: 'mysql',
|
client: 'mysql',
|
||||||
connection: {
|
connection: {
|
||||||
|
|||||||
@@ -30,5 +30,7 @@ export function curateMedia(media, context = {}) {
|
|||||||
parent: media.entity_parent,
|
parent: media.entity_parent,
|
||||||
}),
|
}),
|
||||||
type: context.type || null,
|
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 { fetchTagsById } from './tags.js';
|
||||||
import { fetchEntitiesById } from './entities.js';
|
import { fetchEntitiesById } from './entities.js';
|
||||||
import { curateStash } from './stashes.js';
|
import { curateStash } from './stashes.js';
|
||||||
|
import { censor } from './censor.js';
|
||||||
import escape from '../utils/escape-manticore.js';
|
import escape from '../utils/escape-manticore.js';
|
||||||
import promiseProps from '../utils/promise-props.js';
|
import promiseProps from '../utils/promise-props.js';
|
||||||
|
|
||||||
function curateMovie(rawMovie, assets) {
|
function curateMovie(rawMovie, assets, context = {}) {
|
||||||
if (!rawMovie) {
|
if (!rawMovie) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: rawMovie.id,
|
id: rawMovie.id,
|
||||||
title: rawMovie.title,
|
title: censor(rawMovie.title, context.restriction),
|
||||||
slug: rawMovie.slug,
|
slug: rawMovie.slug,
|
||||||
url: rawMovie.url,
|
url: rawMovie.url,
|
||||||
date: rawMovie.date,
|
date: rawMovie.date,
|
||||||
datePrecision: rawMovie.date_precision,
|
datePrecision: rawMovie.date_precision,
|
||||||
createdAt: rawMovie.created_at,
|
createdAt: rawMovie.created_at,
|
||||||
effectiveDate: rawMovie.effective_date,
|
effectiveDate: rawMovie.effective_date,
|
||||||
description: rawMovie.description,
|
description: censor(rawMovie.description, context.restriction),
|
||||||
duration: rawMovie.duration,
|
duration: rawMovie.duration,
|
||||||
channel: {
|
channel: {
|
||||||
id: assets.channel.id,
|
id: assets.channel.id,
|
||||||
slug: assets.channel.slug,
|
slug: assets.channel.slug,
|
||||||
name: assets.channel.name,
|
name: censor(assets.channel.name, context.restriction),
|
||||||
type: assets.channel.type,
|
type: assets.channel.type,
|
||||||
isIndependent: assets.channel.independent,
|
isIndependent: assets.channel.independent,
|
||||||
hasLogo: assets.channel.has_logo,
|
hasLogo: assets.channel.has_logo,
|
||||||
@@ -38,7 +39,7 @@ function curateMovie(rawMovie, assets) {
|
|||||||
network: assets.channel.network_id ? {
|
network: assets.channel.network_id ? {
|
||||||
id: assets.channel.network_id,
|
id: assets.channel.network_id,
|
||||||
slug: assets.channel.network_slug,
|
slug: assets.channel.network_slug,
|
||||||
name: assets.channel.network_name,
|
name: censor(assets.channel.network_name, context.restriction),
|
||||||
type: assets.channel.network_type,
|
type: assets.channel.network_type,
|
||||||
hasLogo: assets.channel.has_logo,
|
hasLogo: assets.channel.has_logo,
|
||||||
} : null,
|
} : null,
|
||||||
@@ -51,7 +52,7 @@ function curateMovie(rawMovie, assets) {
|
|||||||
tags: assets.tags.map((tag) => ({
|
tags: assets.tags.map((tag) => ({
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
slug: tag.slug,
|
slug: tag.slug,
|
||||||
name: tag.name,
|
name: censor(tag.name, context.restriction),
|
||||||
})),
|
})),
|
||||||
// poster: curateMedia(assets.poster),
|
// poster: curateMedia(assets.poster),
|
||||||
covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })),
|
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 {
|
const {
|
||||||
movies,
|
movies,
|
||||||
channels,
|
channels,
|
||||||
@@ -123,20 +124,25 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
|||||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||||
.orderBy('priority', 'desc'),
|
.orderBy('priority', 'desc'),
|
||||||
covers: knex('movies_covers')
|
covers: knex('movies_covers')
|
||||||
|
.select('media.*', 'movies_covers.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||||
.whereIn('movie_id', movieIds)
|
.whereIn('movie_id', movieIds)
|
||||||
.leftJoin('media', 'media.id', 'movies_covers.media_id')
|
.leftJoin('media', 'media.id', 'movies_covers.media_id')
|
||||||
.orderBy('media.index'),
|
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||||
photos: knex.transaction(async (trx) => {
|
.orderBy('media.index')
|
||||||
|
.groupBy('media.id', 'movies_covers.movie_id', 'sfw_media.id'),
|
||||||
|
photos: context.restriction ? [] : knex.transaction(async (trx) => {
|
||||||
if (reqUser) {
|
if (reqUser) {
|
||||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return trx('movies_scenes')
|
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)
|
.whereIn('movies_scenes.movie_id', movieIds)
|
||||||
.whereNotNull('media.id')
|
.whereNotNull('media.id')
|
||||||
.leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_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) => {
|
caps: knex.transaction(async (trx) => {
|
||||||
if (reqUser) {
|
if (reqUser) {
|
||||||
@@ -144,11 +150,13 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return trx('movies_scenes')
|
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)
|
.whereIn('movies_scenes.movie_id', movieIds)
|
||||||
.whereNotNull('media.id')
|
.whereNotNull('media.id')
|
||||||
.leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_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')
|
trailers: knex('movies_trailers')
|
||||||
.whereIn('movie_id', movieIds)
|
.whereIn('movie_id', movieIds)
|
||||||
@@ -192,7 +200,7 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
|||||||
caps: movieCaps,
|
caps: movieCaps,
|
||||||
trailer: movieTrailer,
|
trailer: movieTrailer,
|
||||||
stashes: movieStashes,
|
stashes: movieStashes,
|
||||||
});
|
}, context);
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +406,7 @@ function countAggregations(buckets) {
|
|||||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
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);
|
const options = curateOptions(rawOptions);
|
||||||
|
|
||||||
console.log(options);
|
console.log(options);
|
||||||
@@ -413,13 +421,13 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
|
|||||||
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
||||||
|
|
||||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { shadllow: true, order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
|
||||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
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 }) : [],
|
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 movieIds = result.movies.map((movie) => Number(movie.id));
|
||||||
const movies = await fetchMoviesById(movieIds, reqUser);
|
const movies = await fetchMoviesById(movieIds, reqUser, context);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
movies,
|
movies,
|
||||||
|
|||||||
241
src/scenes.js
@@ -1,7 +1,7 @@
|
|||||||
import config from 'config';
|
import config from 'config';
|
||||||
import util from 'util'; /* eslint-disable-line no-unused-vars */
|
|
||||||
import { MerkleJson } from 'merkle-json';
|
import { MerkleJson } from 'merkle-json';
|
||||||
|
|
||||||
|
import argv from './argv.js';
|
||||||
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
|
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
|
||||||
import { utilsApi } from './manticore.js';
|
import { utilsApi } from './manticore.js';
|
||||||
import { HttpError } from './errors.js';
|
import { HttpError } from './errors.js';
|
||||||
@@ -15,77 +15,35 @@ import escape from '../utils/escape-manticore.js';
|
|||||||
import promiseProps from '../utils/promise-props.js';
|
import promiseProps from '../utils/promise-props.js';
|
||||||
import initLogger from './logger.js';
|
import initLogger from './logger.js';
|
||||||
import { curateRevision } from './revisions.js';
|
import { curateRevision } from './revisions.js';
|
||||||
|
import { getAffiliateSceneUrl } from './affiliates.js';
|
||||||
|
import { censor } from './censor.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
const mj = new MerkleJson();
|
const mj = new MerkleJson();
|
||||||
|
|
||||||
function getWatchUrl(scene) {
|
function curateScene(rawScene, assets, reqUser, context) {
|
||||||
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) {
|
|
||||||
return watchUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scene.affiliate.url?.includes('/track')) { // standard NATS redirect
|
|
||||||
const { pathname, search } = new URL(watchUrl);
|
|
||||||
|
|
||||||
return `${scene.affiliate.url}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scene.affiliate.parameters) { // used by e.g. Bang
|
|
||||||
const newParams = new URLSearchParams({
|
|
||||||
...Object.fromEntries(new URL(watchUrl).searchParams),
|
|
||||||
...Object.fromEntries(new URLSearchParams(scene.affiliate.parameters)),
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${watchUrl}?${newParams.toString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return watchUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function curateScene(rawScene, assets) {
|
|
||||||
if (!rawScene) {
|
if (!rawScene) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const curatedScene = {
|
const curatedScene = {
|
||||||
id: rawScene.id,
|
id: rawScene.id,
|
||||||
title: rawScene.title,
|
title: censor(rawScene.title, context.restriction),
|
||||||
slug: rawScene.slug,
|
slug: rawScene.slug,
|
||||||
url: rawScene.url,
|
url: rawScene.url,
|
||||||
|
entryId: rawScene.entry_id,
|
||||||
date: rawScene.date,
|
date: rawScene.date,
|
||||||
datePrecision: rawScene.date_precision,
|
datePrecision: rawScene.date_precision,
|
||||||
createdAt: rawScene.created_at,
|
createdAt: rawScene.created_at,
|
||||||
effectiveDate: rawScene.effective_date,
|
effectiveDate: rawScene.effective_date,
|
||||||
description: rawScene.description,
|
description: censor(rawScene.description, context.restriction),
|
||||||
duration: rawScene.duration,
|
duration: rawScene.duration,
|
||||||
shootId: rawScene.shoot_id,
|
shootId: rawScene.shoot_id,
|
||||||
productionDate: rawScene.production_date,
|
productionDate: rawScene.production_date,
|
||||||
channel: {
|
channel: {
|
||||||
id: assets.channel.id,
|
id: assets.channel.id,
|
||||||
slug: assets.channel.slug,
|
slug: assets.channel.slug,
|
||||||
name: assets.channel.name,
|
name: censor(assets.channel.name, context.restriction),
|
||||||
type: assets.channel.type,
|
type: assets.channel.type,
|
||||||
isIndependent: assets.channel.independent,
|
isIndependent: assets.channel.independent,
|
||||||
hasLogo: assets.channel.has_logo,
|
hasLogo: assets.channel.has_logo,
|
||||||
@@ -93,7 +51,7 @@ function curateScene(rawScene, assets) {
|
|||||||
network: assets.channel.network_id ? {
|
network: assets.channel.network_id ? {
|
||||||
id: assets.channel.network_id,
|
id: assets.channel.network_id,
|
||||||
slug: assets.channel.network_slug,
|
slug: assets.channel.network_slug,
|
||||||
name: assets.channel.network_name,
|
name: censor(assets.channel.network_name, context.restriction),
|
||||||
type: assets.channel.network_type,
|
type: assets.channel.network_type,
|
||||||
hasLogo: assets.channel.network_has_logo,
|
hasLogo: assets.channel.network_has_logo,
|
||||||
} : null,
|
} : null,
|
||||||
@@ -107,7 +65,8 @@ function curateScene(rawScene, assets) {
|
|||||||
affiliate: assets.channel.affiliate ? {
|
affiliate: assets.channel.affiliate ? {
|
||||||
id: assets.channel.affiliate.id,
|
id: assets.channel.affiliate.id,
|
||||||
url: assets.channel.affiliate.url,
|
url: assets.channel.affiliate.url,
|
||||||
parameters: assets.channel.affiliate.parameters,
|
parameters: assets.channel.affiliate.parameters || {},
|
||||||
|
entityId: assets.channel.affiliate.entity_id,
|
||||||
} : null,
|
} : null,
|
||||||
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, {
|
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, {
|
||||||
sceneDate: rawScene.effective_date,
|
sceneDate: rawScene.effective_date,
|
||||||
@@ -121,15 +80,20 @@ function curateScene(rawScene, assets) {
|
|||||||
tags: assets.tags.map((tag) => ({
|
tags: assets.tags.map((tag) => ({
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
slug: tag.slug,
|
slug: tag.slug,
|
||||||
name: tag.name,
|
name: censor(tag.name, context.restriction),
|
||||||
priority: tag.priority,
|
priority: tag.priority,
|
||||||
|
actorId: tag.actor_id,
|
||||||
})),
|
})),
|
||||||
chapters: assets.chapters.map((chapter) => ({
|
chapters: assets.chapters.map((chapter) => ({
|
||||||
id: chapter.id,
|
id: chapter.id,
|
||||||
title: chapter.title,
|
title: chapter.title,
|
||||||
|
description: chapter.description,
|
||||||
time: chapter.time,
|
time: chapter.time,
|
||||||
|
date: chapter.date,
|
||||||
duration: chapter.duration,
|
duration: chapter.duration,
|
||||||
poster: curateMedia(chapter.chapter_poster),
|
poster: context.restriction
|
||||||
|
? null
|
||||||
|
: curateMedia(chapter.chapter_poster, { type: 'poster' }),
|
||||||
tags: chapter.chapter_tags.map((tag) => ({
|
tags: chapter.chapter_tags.map((tag) => ({
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
@@ -141,27 +105,48 @@ function curateScene(rawScene, assets) {
|
|||||||
movies: assets.movies.map((movie) => ({
|
movies: assets.movies.map((movie) => ({
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
slug: movie.slug,
|
slug: movie.slug,
|
||||||
title: movie.title,
|
title: censor(movie.title, context.restriction),
|
||||||
covers: movie.movie_covers?.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index) || [],
|
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) => ({
|
series: assets.series.map((serie) => ({
|
||||||
id: serie.id,
|
id: serie.id,
|
||||||
slug: serie.slug,
|
slug: serie.slug,
|
||||||
title: serie.title,
|
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' }),
|
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' })) || [],
|
photos: assets.photos?.map((photo) => curateMedia(photo, { type: 'photo' })) || [],
|
||||||
caps: assets.caps?.map((cap) => curateMedia(cap, { type: 'cap' })) || [],
|
caps: assets.caps?.map((cap) => curateMedia(cap, { type: 'cap' })) || [],
|
||||||
stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
|
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,
|
createdBatchId: rawScene.created_batch_id,
|
||||||
updatedBatchId: rawScene.updated_batch_id,
|
updatedBatchId: rawScene.updated_batch_id,
|
||||||
isNew: assets.lastBatchId === rawScene.created_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)) {
|
||||||
|
if (reqUser) {
|
||||||
|
// only show trailers to logged in users to curb S3 traffic
|
||||||
|
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted });
|
||||||
|
}
|
||||||
|
|
||||||
|
curatedScene.teaser = curateMedia(assets.teaser, { type: 'teaser', isRestricted: isVideoRestricted });
|
||||||
|
}
|
||||||
|
|
||||||
|
curatedScene.watchUrl = getAffiliateSceneUrl(curatedScene);
|
||||||
|
|
||||||
return curatedScene;
|
return curatedScene;
|
||||||
}
|
}
|
||||||
@@ -182,8 +167,9 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
caps,
|
caps,
|
||||||
trailers,
|
trailers,
|
||||||
teasers,
|
teasers,
|
||||||
|
fingerprints,
|
||||||
stashes,
|
stashes,
|
||||||
lastBatch: { id: lastBatchId },
|
lastBatch,
|
||||||
} = await promiseProps({
|
} = await promiseProps({
|
||||||
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
scenes: knex('releases').whereIn('releases.id', sceneIds),
|
||||||
channels: knex('releases')
|
channels: knex('releases')
|
||||||
@@ -194,14 +180,22 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
'networks.name as network_name',
|
'networks.name as network_name',
|
||||||
'networks.type as network_type',
|
'networks.type as network_type',
|
||||||
'networks.has_logo as network_has_logo',
|
'networks.has_logo as network_has_logo',
|
||||||
knex.raw('row_to_json(coalesce(channel_affiliates, network_affiliates)) as affiliate'),
|
knex.raw('row_to_json(affiliates) as affiliate'),
|
||||||
)
|
)
|
||||||
.whereIn('releases.id', sceneIds)
|
.whereIn('releases.id', sceneIds)
|
||||||
.leftJoin('entities as channels', 'channels.id', 'releases.entity_id')
|
.leftJoin('entities as channels', 'channels.id', 'releases.entity_id')
|
||||||
.leftJoin('entities as networks', 'networks.id', 'channels.parent_id')
|
.leftJoin('entities as networks', 'networks.id', 'channels.parent_id')
|
||||||
.leftJoin('affiliates as channel_affiliates', 'channel_affiliates.entity_id', 'channels.id')
|
// .leftJoin('affiliates as channel_affiliates', 'channel_affiliates.entity_id', 'channels.id')
|
||||||
.leftJoin('affiliates as network_affiliates', 'network_affiliates.entity_id', 'networks.id')
|
// .leftJoin('affiliates as network_affiliates', 'network_affiliates.entity_id', 'networks.id')
|
||||||
.groupBy('channels.id', 'networks.id', 'channel_affiliates.id', 'network_affiliates.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')
|
studios: knex('releases')
|
||||||
.whereIn('releases.id', sceneIds)
|
.whereIn('releases.id', sceneIds)
|
||||||
.leftJoin('entities as studios', 'studios.id', 'releases.studio_id'),
|
.leftJoin('entities as studios', 'studios.id', 'releases.studio_id'),
|
||||||
@@ -210,19 +204,22 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
.select(
|
.select(
|
||||||
'actors.*',
|
'actors.*',
|
||||||
knex.raw('row_to_json(avatars) as avatar'),
|
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.name as birth_country_name',
|
||||||
'countries.alias as birth_country_alias',
|
'countries.alias as birth_country_alias',
|
||||||
'releases_actors.release_id',
|
'releases_actors.release_id',
|
||||||
)
|
)
|
||||||
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
||||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_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')
|
.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')
|
directors: knex('releases_directors')
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||||
tags: knex('releases_tags')
|
tags: knex('releases_tags')
|
||||||
.select('id', 'slug', 'name', 'priority', 'release_id')
|
.select('tags.id', 'slug', 'name', 'priority', 'release_id', 'actor_id')
|
||||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||||
.whereNotNull('tags.id')
|
.whereNotNull('tags.id')
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
@@ -255,17 +252,23 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
.whereIn('scene_id', sceneIds)
|
.whereIn('scene_id', sceneIds)
|
||||||
.groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [],
|
.groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [],
|
||||||
posters: knex('releases_posters')
|
posters: knex('releases_posters')
|
||||||
|
.select('media.*', 'releases_posters.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
.leftJoin('media', 'media.id', 'releases_posters.media_id')
|
||||||
photos: context.includeAssets ? knex.transaction(async (trx) => {
|
.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) {
|
if (reqUser) {
|
||||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return trx('releases_photos')
|
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', 'media.id', 'releases_photos.media_id')
|
||||||
|
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||||
.whereIn('release_id', sceneIds)
|
.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) => {
|
caps: context.includeAssets ? knex.transaction(async (trx) => {
|
||||||
if (reqUser) {
|
if (reqUser) {
|
||||||
@@ -273,9 +276,12 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return trx('releases_caps')
|
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', 'media.id', 'releases_caps.media_id')
|
||||||
|
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||||
.whereIn('release_id', sceneIds)
|
.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) => {
|
trailers: context.includeAssets ? knex.transaction(async (trx) => {
|
||||||
if (reqUser) {
|
if (reqUser) {
|
||||||
@@ -295,6 +301,20 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('media', 'media.id', 'releases_teasers.media_id');
|
.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')
|
lastBatch: knex('batches')
|
||||||
.select('id')
|
.select('id')
|
||||||
.where('showcased', true)
|
.where('showcased', true)
|
||||||
@@ -335,6 +355,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
const sceneCaps = caps.filter((cap) => cap.release_id === sceneId);
|
const sceneCaps = caps.filter((cap) => cap.release_id === sceneId);
|
||||||
const sceneTrailers = trailers.find((trailer) => trailer.release_id === sceneId);
|
const sceneTrailers = trailers.find((trailer) => trailer.release_id === sceneId);
|
||||||
const sceneTeasers = teasers.find((teaser) => teaser.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 sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
|
||||||
const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
|
const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
|
||||||
|
|
||||||
@@ -352,10 +373,11 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
caps: sceneCaps,
|
caps: sceneCaps,
|
||||||
trailer: sceneTrailers,
|
trailer: sceneTrailers,
|
||||||
teaser: sceneTeasers,
|
teaser: sceneTeasers,
|
||||||
|
fingerprints: sceneFingerprints,
|
||||||
stashes: sceneStashes,
|
stashes: sceneStashes,
|
||||||
actorStashes: sceneActorStashes,
|
actorStashes: sceneActorStashes,
|
||||||
lastBatchId,
|
lastBatchId: lastBatch?.id,
|
||||||
});
|
}, reqUser, context);
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +456,11 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
year(scenes.effective_date) as effective_year,
|
year(scenes.effective_date) as effective_year,
|
||||||
weight() as _score
|
weight() as _score
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
// manticore only supports one joined table, so we can't use it inside stashes; probably not needed anyway (stashes only need global tags?)
|
||||||
|
builder
|
||||||
|
.leftJoin('scenes_tags', 'scenes_tags.scene_id', 'scenes_.id')
|
||||||
|
.groupBy('scenes.id');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.query) {
|
if (filters.query) {
|
||||||
@@ -488,9 +515,11 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
builder.where('scenes.is_showcased', filters.isShowcased);
|
builder.where('scenes.is_showcased', filters.isShowcased);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
if (filters.isShowcased) {
|
if (filters.isShowcased) {
|
||||||
builder.where('scenes.date', '>', 0);
|
builder.where('scenes.date', '>', 0);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (options.dedupe) {
|
if (options.dedupe) {
|
||||||
builder.where('scenes.dupe_index', '<', 2);
|
builder.where('scenes.dupe_index', '<', 2);
|
||||||
@@ -534,11 +563,16 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
.limit(options.limit)
|
.limit(options.limit)
|
||||||
.offset((options.page - 1) * options.limit),
|
.offset((options.page - 1) * options.limit),
|
||||||
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
|
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
|
||||||
yearsFacet: options.aggregateYears ? knex.raw('facet effective_year as years order by effective_year desc limit ?', [aggSize]) : null,
|
yearsFacet: options.aggregateYears ? knex.raw('facet effective_year as years_facet order by effective_year desc limit ?', [aggSize]) : null,
|
||||||
actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids order by count(*) desc limit ?', [aggSize]) : null,
|
actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids as actors_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
|
||||||
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids order by count(*) desc limit ?', [aggSize]) : null,
|
// don't facet tags associated to other actors, actor ID 0 means global
|
||||||
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id order by count(*) desc limit ?', [aggSize]) : null,
|
tagsFacet: options.aggregateTags // eslint-disable-line no-nested-ternary
|
||||||
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id order by count(*) desc limit ?', [aggSize]) : null,
|
? (filters.stashId || !filters?.actorIds || filters.actorIds.length === 0 // we can't join the tags table as well as the stashes table
|
||||||
|
? knex.raw('facet scenes.tag_ids as tags_facet order by count(distinct id) desc limit ?', [aggSize])
|
||||||
|
: knex.raw(`facet IF(IN(scenes_tags.actor_id, ${[0, ...filters?.actorIds || []]}), scenes_tags.tag_id, 0) tags_facet distinct id order by count(distinct id) desc limit ?`, [aggSize]))
|
||||||
|
: null,
|
||||||
|
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id as channels_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
|
||||||
|
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id as studios_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
|
||||||
maxMatches: config.database.manticore.maxMatches,
|
maxMatches: config.database.manticore.maxMatches,
|
||||||
maxQueryTime: config.database.manticore.maxQueryTime,
|
maxQueryTime: config.database.manticore.maxQueryTime,
|
||||||
}).toString();
|
}).toString();
|
||||||
@@ -546,9 +580,11 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
|
// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
|
||||||
const curatedSqlQuery = filters.stashId
|
const curatedSqlQuery = filters.stashId
|
||||||
? sqlQuery
|
? sqlQuery
|
||||||
: sqlQuery.replace(/scenes\./g, '');
|
: sqlQuery
|
||||||
|
.replace(/scenes\./g, '')
|
||||||
|
.replace(/scenes_\./g, 'scenes.');
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development' && argv.debug) {
|
||||||
console.log(curatedSqlQuery);
|
console.log(curatedSqlQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,30 +593,28 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
// console.log(util.inspect(results, null, Infinity));
|
// console.log(util.inspect(results, null, Infinity));
|
||||||
|
|
||||||
const years = results
|
const years = results
|
||||||
.find((result) => (result.columns[0].years || result.columns[0]['scenes.years']) && result.columns[1]['count(*)'])
|
.find((result) => result.columns[0].years_facet && result.columns[1]['count(*)'])
|
||||||
?.data.map((row) => ({ key: row.years || row['scenes.years'], doc_count: row['count(*)'] }))
|
?.data.map((row) => ({ key: row.years_facet, doc_count: row['count(*)'] }))
|
||||||
|| [];
|
|| [];
|
||||||
|
|
||||||
const actorIds = results
|
const actorIds = results
|
||||||
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
|
.find((result) => result.columns[0].actors_facet && result.columns[1]['count(distinct id)'])
|
||||||
?.data.map((row) => ({ key: row.actor_ids || row['scenes.actor_ids'], doc_count: row['count(*)'] }))
|
?.data.map((row) => ({ key: row.actors_facet, doc_count: row['count(distinct id)'] }))
|
||||||
|| [];
|
|| [];
|
||||||
|
|
||||||
const tagIds = results
|
const tagIds = results
|
||||||
.find((result) => (result.columns[0].tag_ids || result.columns[0]['scenes.tag_ids']) && result.columns[1]['count(*)'])
|
.find((result) => result.columns[0].tags_facet && result.columns[1]['count(distinct id)'])
|
||||||
?.data.map((row) => ({ key: row.tag_ids || row['scenes.tag_ids'], doc_count: row['count(*)'] }))
|
?.data.map((row) => ({ key: row.tags_facet, doc_count: row['count(distinct id)'] || row['count(*)'] }))
|
||||||
|| [];
|
|| [];
|
||||||
|
|
||||||
const channelIds = results
|
const channelIds = results
|
||||||
.find((result) => (result.columns[0].channel_id || result.columns[0]['scenes.channel_id']) && result.columns[1]['count(*)'])
|
.find((result) => result.columns[0].channels_facet && result.columns[1]['count(distinct id)'])
|
||||||
?.data.map((row) => ({ key: row.channel_id || row['scenes.channel_id'], doc_count: row['count(*)'] }))
|
?.data.map((row) => ({ key: row.channels_facet || row['scenes.channel_id'], doc_count: row['count(distinct id)'] }))
|
||||||
|| [];
|
|| [];
|
||||||
|
|
||||||
const studioIds = results
|
const studioIds = results
|
||||||
.find((result) => (result.columns[0].studio_id || result.columns[0]['scenes.studio_id']) && result.columns[1]['count(*)'])
|
.find((result) => result.columns[0].studios_facet && result.columns[1]['count(distinct id)'])
|
||||||
?.data
|
?.data.map((row) => ({ key: row.studios_facet, doc_count: row['count(distinct id)'] })).filter((row) => !!row.key)
|
||||||
.map((row) => ({ key: row.studio_id || row['scenes.studio_id'], doc_count: row['count(*)'] }))
|
|
||||||
.filter((row) => !!row.key)
|
|
||||||
|| [];
|
|| [];
|
||||||
|
|
||||||
const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found')?.Value) || 0;
|
const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found')?.Value) || 0;
|
||||||
@@ -606,14 +640,18 @@ function countAggregations(buckets) {
|
|||||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
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);
|
const options = curateOptions(rawOptions);
|
||||||
|
|
||||||
console.log('filters', filters);
|
if (argv.debug) {
|
||||||
console.log('options', options);
|
console.log('filters', filters);
|
||||||
|
console.log('options', options);
|
||||||
|
}
|
||||||
|
|
||||||
console.time('manticore sql');
|
console.time('manticore sql');
|
||||||
|
|
||||||
const result = await queryManticoreSql(filters, options, reqUser);
|
const result = await queryManticoreSql(filters, options, reqUser);
|
||||||
|
|
||||||
console.timeEnd('manticore sql');
|
console.timeEnd('manticore sql');
|
||||||
|
|
||||||
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
|
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
|
||||||
@@ -626,16 +664,16 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
|||||||
console.time('fetch aggregations');
|
console.time('fetch aggregations');
|
||||||
|
|
||||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { shallow: true, order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
|
||||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
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 }) : [],
|
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.timeEnd('fetch aggregations');
|
console.timeEnd('fetch aggregations');
|
||||||
|
|
||||||
console.time('fetch full');
|
console.time('fetch full');
|
||||||
const sceneIds = result.scenes.map((scene) => Number(scene.id));
|
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');
|
console.timeEnd('fetch full');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -757,9 +795,10 @@ async function applySceneTagsDelta(sceneId, delta, trx) {
|
|||||||
|
|
||||||
if (delta.value.length > 0) {
|
if (delta.value.length > 0) {
|
||||||
await knexOwner('releases_tags')
|
await knexOwner('releases_tags')
|
||||||
.insert(delta.value.map((tagId) => ({
|
.insert(delta.value.map((tag) => ({
|
||||||
release_id: sceneId,
|
release_id: sceneId,
|
||||||
tag_id: tagId,
|
tag_id: tag.id,
|
||||||
|
actor_id: tag.actorId,
|
||||||
source: 'editor',
|
source: 'editor',
|
||||||
})))
|
})))
|
||||||
.transacting(trx);
|
.transacting(trx);
|
||||||
|
|||||||
27
src/tags.js
@@ -1,17 +1,18 @@
|
|||||||
import knex from './knex.js';
|
import knex from './knex.js';
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
import initLogger from './logger.js';
|
import initLogger from './logger.js';
|
||||||
|
import { censor } from './censor.js';
|
||||||
|
|
||||||
import { curateMedia } from './media.js';
|
import { curateMedia } from './media.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
|
|
||||||
function curateTag(tag, context) {
|
function curateTag(tag, context = {}) {
|
||||||
return {
|
return {
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
name: tag.name,
|
name: censor(tag.name, context.restriction),
|
||||||
slug: tag.slug,
|
slug: tag.slug,
|
||||||
description: tag.description,
|
description: context.restriction ? null : tag.description, // censor interferes with markdown
|
||||||
priority: tag.priority,
|
priority: tag.priority,
|
||||||
poster: tag.poster && curateMedia(tag.poster),
|
poster: tag.poster && curateMedia(tag.poster),
|
||||||
photos: tag.photos?.map((photo) => curateMedia(photo)) || [],
|
photos: tag.photos?.map((photo) => curateMedia(photo)) || [],
|
||||||
@@ -23,7 +24,7 @@ function curateTag(tag, context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTags(options = {}) {
|
export async function fetchTags(options = {}, context = {}) {
|
||||||
const query = options.query?.trim();
|
const query = options.query?.trim();
|
||||||
|
|
||||||
const [tags, posters] = await Promise.all([
|
const [tags, posters] = await Promise.all([
|
||||||
@@ -55,10 +56,13 @@ export async function fetchTags(options = {}) {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
knex('tags_posters')
|
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'))
|
.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', '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', '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]));
|
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
|
||||||
@@ -66,10 +70,10 @@ export async function fetchTags(options = {}) {
|
|||||||
return tags.map((tagEntry) => curateTag({
|
return tags.map((tagEntry) => curateTag({
|
||||||
...tagEntry,
|
...tagEntry,
|
||||||
poster: postersByTagId[tagEntry.id],
|
poster: postersByTagId[tagEntry.id],
|
||||||
}));
|
}, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
export async function fetchTagsById(tagIds, options = {}, reqUser, context = {}) {
|
||||||
const [tags, posters, photos, alerts] = await Promise.all([
|
const [tags, posters, photos, alerts] = await Promise.all([
|
||||||
knex('tags')
|
knex('tags')
|
||||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||||
@@ -80,14 +84,17 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
knex('tags_posters')
|
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'))
|
.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('tags', 'tags.id', 'tags_posters.tag_id')
|
||||||
.leftJoin('media', 'media.id', 'tags_posters.media_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', 'entities.id', 'media.entity_id')
|
||||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string')),
|
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string'))
|
||||||
knex('tags_photos')
|
.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'))
|
.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('tags', 'tags.id', 'tags_photos.tag_id')
|
||||||
.leftJoin('media', 'media.id', 'tags_photos.media_id')
|
.leftJoin('media', 'media.id', 'tags_photos.media_id')
|
||||||
@@ -118,6 +125,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
|||||||
}, {
|
}, {
|
||||||
alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id),
|
alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id),
|
||||||
append: options.append,
|
append: options.append,
|
||||||
|
...context,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +144,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
|||||||
}, {
|
}, {
|
||||||
alerts: alerts.filter((alert) => alert.tag_id === tag.id),
|
alerts: alerts.filter((alert) => alert.tag_id === tag.id),
|
||||||
append: options.append,
|
append: options.append,
|
||||||
|
...context,
|
||||||
});
|
});
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
|||||||
18
src/users.js
@@ -29,6 +29,7 @@ export function curateUser(user, _assets = {}) {
|
|||||||
isIdentityVerified: user.identity_verified,
|
isIdentityVerified: user.identity_verified,
|
||||||
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
abilities: [...user.role_abilities || [], ...user.abilities || []],
|
||||||
createdAt: user.created_at,
|
createdAt: user.created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,23 +62,6 @@ export async function fetchUser(userId, options = {}, _reqUser) {
|
|||||||
throw new HttpError(`User '${userId}' not found`, 404);
|
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) {
|
if (options.raw) {
|
||||||
// return { user, stashes, templates };
|
// return { user, stashes, templates };
|
||||||
return { user };
|
return { user };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
||||||
import IPCIDR from 'ip-cidr';
|
import IPCIDR from 'ip-cidr';
|
||||||
|
import argv from '../argv.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
login,
|
login,
|
||||||
@@ -14,6 +15,10 @@ import {
|
|||||||
import { fetchUser } from '../users.js';
|
import { fetchUser } from '../users.js';
|
||||||
|
|
||||||
function getIp(req) {
|
function getIp(req) {
|
||||||
|
if (argv.ip) {
|
||||||
|
return argv.ip;
|
||||||
|
}
|
||||||
|
|
||||||
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
|
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
|
||||||
|
|
||||||
const unmappedIp = ip?.includes('.')
|
const unmappedIp = ip?.includes('.')
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default async function mainHandler(req, res, next) {
|
|||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
role: req.user.role,
|
role: req.user.role,
|
||||||
|
abilities: req.user.abilities,
|
||||||
avatar: req.user.avatar,
|
avatar: req.user.avatar,
|
||||||
},
|
},
|
||||||
assets: req.user ? {
|
assets: req.user ? {
|
||||||
@@ -45,7 +46,12 @@ export default async function mainHandler(req, res, next) {
|
|||||||
psa: config.psa,
|
psa: config.psa,
|
||||||
links: config.links,
|
links: config.links,
|
||||||
socials,
|
socials,
|
||||||
|
captcha: {
|
||||||
|
enabled: config.auth.captcha.enabled,
|
||||||
|
siteKey: config.auth.captcha.siteKey,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
restriction: req.restriction,
|
||||||
meta: {
|
meta: {
|
||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export async function fetchMoviesApi(req, res) {
|
|||||||
} = await fetchMovies(await curateMoviesQuery(req.query), {
|
} = await fetchMovies(await curateMoviesQuery(req.query), {
|
||||||
page: Number(req.query.page) || 1,
|
page: Number(req.query.page) || 1,
|
||||||
limit: Number(req.query.limit) || 30,
|
limit: Number(req.query.limit) || 30,
|
||||||
}, req.user);
|
}, req.user, { restriction: req.restriction });
|
||||||
|
|
||||||
res.send(stringify({
|
res.send(stringify({
|
||||||
movies,
|
movies,
|
||||||
@@ -47,7 +47,7 @@ export async function fetchMoviesApi(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMovieApi(req, res) {
|
export async function fetchMovieApi(req, res) {
|
||||||
const [movie] = await fetchMoviesById([Number(req.params.movieId)], { reqUser: req.user });
|
const [movie] = await fetchMoviesById([Number(req.params.movieId)], { reqUser: req.user }, { restriction: req.restriction });
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404);
|
throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404);
|
||||||
@@ -137,7 +137,7 @@ export async function fetchMoviesGraphql(query, req) {
|
|||||||
page: query.page || 1,
|
page: query.page || 1,
|
||||||
limit: query.limit || 30,
|
limit: query.limit || 30,
|
||||||
aggregate: false,
|
aggregate: false,
|
||||||
}, req.user);
|
}, req.user, { restriction: req.restriction });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: movies,
|
nodes: movies,
|
||||||
|
|||||||
84
src/web/restrictions.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import config from 'config';
|
||||||
|
import path from 'path';
|
||||||
|
import { Reader } from '@maxmind/geoip2-node';
|
||||||
|
|
||||||
|
import initLogger from '../logger.js';
|
||||||
|
|
||||||
|
const logger = initLogger();
|
||||||
|
const regions = config.restrictions.regions;
|
||||||
|
|
||||||
|
export default async function initRestrictionHandler() {
|
||||||
|
const reader = await Reader.open('assets/GeoLite2-City.mmdb');
|
||||||
|
|
||||||
|
function getRestriction(req) {
|
||||||
|
if (Object.hasOwn(req.session, 'restriction') && Object.hasOwn(req.session, 'country') && req.session.restrictionIp === req.userIp) {
|
||||||
|
return {
|
||||||
|
restriction: req.session.restriction,
|
||||||
|
country: req.session.country,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = reader.city(req.userIp);
|
||||||
|
const country = location.country.isoCode;
|
||||||
|
const subdivision = location.subdivisions?.[0]?.isoCode;
|
||||||
|
|
||||||
|
if (regions[country]?.[subdivision]) {
|
||||||
|
// state or province restriction
|
||||||
|
return {
|
||||||
|
restriction: config.restrictions.modes[regions[country][subdivision]],
|
||||||
|
country,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regions[country]) {
|
||||||
|
// country restriction
|
||||||
|
return {
|
||||||
|
restriction: config.restrictions.modes[regions[country]],
|
||||||
|
country,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
restriction: null,
|
||||||
|
country,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restrictionHandler(req, res, next) {
|
||||||
|
if (!config.restrictions.enabled) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { restriction, country } = getRestriction(req);
|
||||||
|
|
||||||
|
if (restriction === 'block' || req.path === '/sfw/') {
|
||||||
|
res.render(path.join(import.meta.dirname, '../../assets/sfw.ejs'), {
|
||||||
|
noVpn: config.restrictions.noVpn.includes(country),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.session.restriction !== restriction) {
|
||||||
|
req.session.restrictionIp = req.userIp;
|
||||||
|
req.session.restriction = restriction;
|
||||||
|
req.session.country = country;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.restriction = restriction;
|
||||||
|
req.country = country;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed Maxmind IP lookup for ${req.ip}: ${error.message}`);
|
||||||
|
|
||||||
|
req.session.restrictionIp = req.userIp;
|
||||||
|
req.session.restriction = 0;
|
||||||
|
req.session.country = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return restrictionHandler;
|
||||||
|
}
|
||||||
@@ -68,7 +68,9 @@ async function fetchScenesApi(req, res) {
|
|||||||
}), {
|
}), {
|
||||||
page: Number(req.query.page) || 1,
|
page: Number(req.query.page) || 1,
|
||||||
limit: Number(req.query.limit) || 30,
|
limit: Number(req.query.limit) || 30,
|
||||||
}, req.user);
|
}, req.user, {
|
||||||
|
restriction: req.restriction,
|
||||||
|
});
|
||||||
|
|
||||||
res.send(stringify({
|
res.send(stringify({
|
||||||
scenes,
|
scenes,
|
||||||
@@ -250,7 +252,7 @@ export async function fetchScenesGraphql(query, req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSceneApi(req, res) {
|
async function fetchSceneApi(req, res) {
|
||||||
const [scene] = await fetchScenesById([Number(req.params.sceneId)], { reqUser: req.user });
|
const [scene] = await fetchScenesById([Number(req.params.sceneId)], { reqUser: req.user }, { restriction: req.restriction });
|
||||||
|
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
|
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
|
||||||
@@ -263,6 +265,7 @@ export async function fetchScenesByIdGraphql(query, req) {
|
|||||||
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
||||||
reqUser: req.user,
|
reqUser: req.user,
|
||||||
includePartOf: true,
|
includePartOf: true,
|
||||||
|
restriction: req.restriction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (query.ids) {
|
if (query.ids) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import redis from '../redis.js';
|
|||||||
|
|
||||||
import errorHandler from './error.js';
|
import errorHandler from './error.js';
|
||||||
import consentHandler from './consent.js';
|
import consentHandler from './consent.js';
|
||||||
|
import initRestrictionHandler from './restrictions.js';
|
||||||
|
|
||||||
import { scenesRouter } from './scenes.js';
|
import { scenesRouter } from './scenes.js';
|
||||||
import { actorsRouter } from './actors.js';
|
import { actorsRouter } from './actors.js';
|
||||||
@@ -40,6 +41,8 @@ import { router as userRouter } from './users.js';
|
|||||||
import { router as stashesRouter } from './stashes.js';
|
import { router as stashesRouter } from './stashes.js';
|
||||||
import { router as alertsRouter } from './alerts.js';
|
import { router as alertsRouter } from './alerts.js';
|
||||||
|
|
||||||
|
import { initCachesApi } from './system.js';
|
||||||
|
|
||||||
import initLogger from '../logger.js';
|
import initLogger from '../logger.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
@@ -48,9 +51,11 @@ const isProduction = process.env.NODE_ENV === 'production';
|
|||||||
export default async function initServer() {
|
export default async function initServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
const restrictionHandler = await initRestrictionHandler();
|
||||||
|
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
router.use(boolParser());
|
router.use(boolParser());
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ export default async function initServer() {
|
|||||||
router.use('/', express.static('static'));
|
router.use('/', express.static('static'));
|
||||||
router.use('/media', express.static(config.media.path));
|
router.use('/media', express.static(config.media.path));
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, _res, next) => {
|
||||||
if (req.headers.cookie) {
|
if (req.headers.cookie) {
|
||||||
const cookies = cookie.parse(req.headers.cookie);
|
const cookies = cookie.parse(req.headers.cookie);
|
||||||
|
|
||||||
@@ -109,11 +114,13 @@ export default async function initServer() {
|
|||||||
router.use(viteDevMiddleware);
|
router.use(viteDevMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/consent', (req, res) => {
|
router.use(restrictionHandler);
|
||||||
|
|
||||||
|
router.get('/consent', (_req, res) => {
|
||||||
res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html'));
|
res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use('/api/*', async (req, res, next) => {
|
router.use('/api/*', async (req, _res, next) => {
|
||||||
if (req.headers['api-user']) {
|
if (req.headers['api-user']) {
|
||||||
await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
||||||
|
|
||||||
@@ -153,13 +160,15 @@ export default async function initServer() {
|
|||||||
// TAGS
|
// TAGS
|
||||||
router.get('/api/tags', fetchTagsApi);
|
router.get('/api/tags', fetchTagsApi);
|
||||||
|
|
||||||
|
router.post('/api/caches', initCachesApi);
|
||||||
|
|
||||||
if (config.apiAccess.graphqlEnabled) {
|
if (config.apiAccess.graphqlEnabled) {
|
||||||
router.post('/graphql', graphqlApi);
|
router.post('/graphql', graphqlApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.use(consentHandler);
|
router.use(consentHandler);
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((_req, res, next) => {
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
res.set('Accept-CH', 'Sec-CH-Prefers-Color-Scheme');
|
res.set('Accept-CH', 'Sec-CH-Prefers-Color-Scheme');
|
||||||
res.set('Vary', 'Sec-CH-Prefers-Color-Scheme');
|
res.set('Vary', 'Sec-CH-Prefers-Color-Scheme');
|
||||||
@@ -175,7 +184,9 @@ export default async function initServer() {
|
|||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
const port = process.env.PORT || config.web.port || 3000;
|
const port = process.env.PORT || config.web.port || 3000;
|
||||||
app.listen(port);
|
// const port = Math.round(Math.random() * 10000);
|
||||||
|
|
||||||
logger.info(`Server running at http://localhost:${port}`);
|
app.listen(port, config.web.host);
|
||||||
|
|
||||||
|
logger.info(`Server running at http://${config.web.host}:${port}`);
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/web/system.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { HttpError } from '../errors.js';
|
||||||
|
import { initCaches } from '../cache.js';
|
||||||
|
|
||||||
|
export async function initCachesApi(req, res) {
|
||||||
|
if (req.user?.role !== 'admin') {
|
||||||
|
throw new HttpError('You must be an admin to initialize caches', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
await initCaches();
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import { fetchTags } from '../tags.js';
|
|||||||
export async function fetchTagsApi(req, res) {
|
export async function fetchTagsApi(req, res) {
|
||||||
const tags = await fetchTags({
|
const tags = await fetchTags({
|
||||||
query: req.query.query,
|
query: req.query.query,
|
||||||
|
}, {
|
||||||
|
restriction: req.restriction,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(tags);
|
res.send(tags);
|
||||||
|
|||||||