Compare commits
80 Commits
5194c5e156
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 27ce8b0ceb | |||
| 0e5724533f | |||
| cea58d12ff | |||
| 25034e7a4b | |||
| cd4a7ce9c8 | |||
| 2229255ff4 | |||
| 299dbe3239 | |||
| deced84c59 | |||
| 0150ae8d1c | |||
| 6877ee75ed | |||
| 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 |
@@ -21,7 +21,7 @@
|
||||
"no-console": 0,
|
||||
"no-param-reassign": ["error", {
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": ["state", "acc"]
|
||||
"ignorePropertyModificationsFor": ["state", "acc", "req"]
|
||||
}],
|
||||
"vue/multi-word-component-names": 0,
|
||||
"vue/no-reserved-component-names": 0,
|
||||
@@ -32,7 +32,8 @@
|
||||
"vue/html-indent": ["error", "tab"],
|
||||
"vue/multiline-html-element-content-newline": 0,
|
||||
"vue/no-v-html": 0,
|
||||
"vue/singleline-html-element-content-newline": 0
|
||||
"vue/singleline-html-element-content-newline": 0,
|
||||
"vue/comment-directive": 0,
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
|
||||
2
.gitignore
vendored
@@ -5,3 +5,5 @@ config/*
|
||||
log/
|
||||
/media
|
||||
data/
|
||||
assets/*.mmdb
|
||||
assets/.geoipupdate.lock
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
|
||||
--text: #222;
|
||||
--text-light: #fff;
|
||||
--text-piss: #b92;
|
||||
|
||||
/* --link: #48f; */
|
||||
--link: var(--primary);
|
||||
@@ -101,6 +102,7 @@
|
||||
--background-dim: var(--shadow-weak-10);
|
||||
|
||||
--text: #fcfcfc;
|
||||
--text-piss: #ba0;
|
||||
|
||||
--glass-weak-50: rgba(255, 255, 255, .02);
|
||||
--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 |
27
assets/img/icons/user-tags.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<path
|
||||
id="path2"
|
||||
d="M 7 0 C 4 0 4 2.015 4 4.5 C 4 6.048 4.898 7.5957969 6 8.2167969 L 6 9.0410156 C 2.608 9.3180156 0 10.985 0 13 L 4.9726562 13 C 4.6689986 12.449922 4.7486357 11.731833 5.2109375 11.269531 L 8 8.4804688 L 8 8.2167969 C 9.102 7.5957969 10 6.048 10 4.5 C 10 2.015 10 0 7 0 z M 9 11.509766 L 8.2167969 12.292969 L 8.9238281 13 L 9 13 L 9 11.509766 z " />
|
||||
<g
|
||||
id="g2"
|
||||
transform="matrix(0.51568847,0,0,0.51568847,5.8471254,8.4255678)">
|
||||
<path
|
||||
d="m 18.938,-1 h -6 c -0.412,0 -0.989,0.239 -1.28,0.53 L 4.219,6.969 c -0.292,0.292 -0.292,0.769 0,1.061 l 6.439,6.439 c 0.292,0.292 0.769,0.292 1.061,0 L 19.158,7.03 c 0.292,-0.292 0.53,-0.868 0.53,-1.28 v -6 c 0,-0.412 -0.337,-0.75 -0.75,-0.75 z m -3.75,6 c -0.828,0 -1.5,-0.672 -1.5,-1.5 0,-0.828 0.672,-1.5 1.5,-1.5 0.828,0 1.5,0.672 1.5,1.5 0,0.828 -0.672,1.5 -1.5,1.5 z"
|
||||
id="path1-5" />
|
||||
<path
|
||||
d="m 1.688,7.5 8.5,-8.5 h -1.25 c -0.412,0 -0.989,0.239 -1.28,0.53 L 0.219,6.969 c -0.292,0.292 -0.292,0.769 0,1.061 l 6.439,6.439 c 0.292,0.292 0.769,0.292 1.061,0 l 0.47,-0.47 -6.5,-6.5 z"
|
||||
id="path2-3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
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>
|
||||
@@ -11,7 +11,7 @@
|
||||
class="avatar-link no-link"
|
||||
>
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
v-if="actor.avatar && !restriction"
|
||||
:src="getPath(actor.avatar, 'thumbnail')"
|
||||
:style="{ 'background-image': `url(${getPath(actor.avatar, 'lazy')})` }"
|
||||
loading="lazy"
|
||||
@@ -103,7 +103,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user } = pageContext;
|
||||
const { user, restriction } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
>Entity Health</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="initCaches"
|
||||
><Icon icon="database-refresh" />Reload caches</button>
|
||||
</nav>
|
||||
|
||||
<div class="content">
|
||||
@@ -37,7 +42,17 @@
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
async function initCaches() {
|
||||
await post('/caches', null, {
|
||||
successFeedback: 'Reloaded caches',
|
||||
errorFeedback: 'Failed to reload caches',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -50,6 +65,7 @@ const pageContext = inject('pageContext');
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1rem .75rem 1rem;
|
||||
border-bottom: solid 1px var(--shadow-weak-30);
|
||||
margin-bottom: .25rem;
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="restriction"
|
||||
class="restricted"
|
||||
>
|
||||
<div>Traxxx is restricted in your region</div>
|
||||
<a
|
||||
href="/sfw"
|
||||
class="link"
|
||||
>Learn more</a>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
v-if="campaign?.banner?.type === 'html'"
|
||||
v-else-if="campaign?.banner?.type === 'html'"
|
||||
ref="iframe"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
@@ -18,28 +29,44 @@
|
||||
:href="campaign.url || campaign.affiliate?.url"
|
||||
target="_blank"
|
||||
class="campaign"
|
||||
:style="{ 'background-image': backdrop && `url(${bannerSrc})` }"
|
||||
:class="{ backdrop }"
|
||||
data-umami-event="campaign-click"
|
||||
:data-umami-event-campaign-id="`${campaign.entity.slug}-${campaign.id}`"
|
||||
>
|
||||
<img
|
||||
:src="bannerSrc"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
class="campaign-banner"
|
||||
>
|
||||
<div class="campaign-overlay">
|
||||
<img
|
||||
:src="bannerSrc"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
class="campaign-banner"
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { restriction } = pageContext;
|
||||
|
||||
const props = defineProps({
|
||||
campaign: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
backdrop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// console.log(props.campaign);
|
||||
|
||||
const bannerSrc = (() => {
|
||||
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) {
|
||||
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||
}
|
||||
@@ -58,7 +85,10 @@ const bannerSrc = (() => {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* align-items: center; */
|
||||
border-radius: .25rem;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.frame {
|
||||
@@ -66,11 +96,45 @@ const bannerSrc = (() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.campaign-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
.campaign-overlay {
|
||||
padding: 4px; /* margin for drop shadow */
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(125%);
|
||||
}
|
||||
|
||||
.dark .campaign-overlay {
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(75%);
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
filter: drop-shadow(0 0 2px var(--shadow-weak-20));
|
||||
}
|
||||
}
|
||||
|
||||
.restricted {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
font-weight: bold;
|
||||
padding: .5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
<template>
|
||||
<ul
|
||||
class="tags nolist"
|
||||
:class="{ disabled: !editing.has(item.key) }"
|
||||
>
|
||||
<li
|
||||
v-for="tag in [...item.value, ...newTags]"
|
||||
:key="`tag-${tag.id}`"
|
||||
class="tag"
|
||||
:class="{ deleted: edits.tags && !edits.tags.some((tagId) => tagId === tag.id) }"
|
||||
<div class="tags-sections">
|
||||
<div
|
||||
v-for="actorTags in tags"
|
||||
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||
class="tags-section"
|
||||
>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
|
||||
<Icon
|
||||
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"
|
||||
<ul
|
||||
class="tags nolist"
|
||||
:class="{ disabled: !editing.has(item.key) }"
|
||||
>
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</TagSearch>
|
||||
</li>
|
||||
</ul>
|
||||
<li
|
||||
v-if="actorTags.actor"
|
||||
class="tags-actor"
|
||||
>{{ actorTags.actor.name }}:</li>
|
||||
|
||||
<li
|
||||
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>
|
||||
|
||||
<script setup>
|
||||
@@ -70,8 +83,23 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['tags']);
|
||||
|
||||
function addTag(tag) {
|
||||
if (props.edits.tags.some((tagId) => tagId === tag.id)) {
|
||||
const tags = [
|
||||
{
|
||||
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', {
|
||||
type: 'error',
|
||||
message: 'Tag already added',
|
||||
@@ -80,9 +108,13 @@ function addTag(tag) {
|
||||
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 = []; });
|
||||
@@ -116,7 +148,7 @@ watch(() => props.scene, () => { newTags.value = []; });
|
||||
align-items: center;
|
||||
margin-left: .25rem;
|
||||
|
||||
&:hover {
|
||||
&:hover .icon {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@@ -145,9 +189,11 @@ watch(() => props.scene, () => { newTags.value = []; });
|
||||
.tag,
|
||||
.new {
|
||||
.remove,
|
||||
.add {
|
||||
.add,
|
||||
.actor {
|
||||
height: auto;
|
||||
padding: .25rem .3rem;
|
||||
margin-left: .25rem;
|
||||
border-radius: .25rem;
|
||||
fill: var(--highlight-strong-10);
|
||||
|
||||
|
||||
@@ -12,14 +12,32 @@
|
||||
<Icon icon="search" />
|
||||
</label>
|
||||
|
||||
<!--
|
||||
<div
|
||||
v-show="showActorTags"
|
||||
v-tooltip="'Tags relevant to the selected actors'"
|
||||
class="filter-sort order noselect"
|
||||
@click="showActorTags = false"
|
||||
>
|
||||
<Icon icon="user-tags" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="!showActorTags"
|
||||
v-tooltip="'All tags'"
|
||||
class="filter-sort order noselect"
|
||||
@click="showActorTags = true"
|
||||
>
|
||||
<Icon icon="price-tags" />
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div
|
||||
v-show="order === 'priority'"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'count'"
|
||||
>
|
||||
<Icon
|
||||
icon="star"
|
||||
/>
|
||||
<Icon icon="star" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -115,16 +133,22 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
actorTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
// const { tag: pageTag, actor: pageActor } = pageProps;
|
||||
const { tag: pageTag } = pageProps;
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const order = ref('priority');
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
const { tag: pageTag } = pageProps;
|
||||
// const showActorTags = ref(true);
|
||||
|
||||
const priorityTags = [
|
||||
'anal',
|
||||
@@ -148,8 +172,15 @@ const priorityTags = [
|
||||
];
|
||||
|
||||
const groupedTags = computed(() => {
|
||||
const selected = props.tags.filter((tag) => props.filters.tags.includes(tag.slug));
|
||||
const filtered = props.tags.filter((tag) => !props.filters.tags.includes(tag.slug)
|
||||
/*
|
||||
const tags = showActorTags.value && props.actorTags && (props.filters.actors.length > 0 || pageActor)
|
||||
? props.actorTags
|
||||
: props.tags;
|
||||
*/
|
||||
const tags = props.tags;
|
||||
|
||||
const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug));
|
||||
const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug)
|
||||
&& tag.id !== pageTag?.id
|
||||
&& searchRegexp.value.test(tag.name));
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
<footer class="footer">
|
||||
<span class="footer-segment">© traxxx</span>
|
||||
|
||||
<a
|
||||
v-if="env.links.matrix"
|
||||
:href="env.links.matrix"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="footer-segment footer-link nolink matrix"
|
||||
><Icon icon="matrix-full" /></a>
|
||||
|
||||
<a
|
||||
v-if="env.links.discord"
|
||||
:href="env.links.discord"
|
||||
@@ -56,7 +64,8 @@ const { env } = pageContext;
|
||||
}
|
||||
}
|
||||
|
||||
.discord .icon {
|
||||
.discord .icon,
|
||||
.matrix .icon {
|
||||
height: 1.25rem;
|
||||
width: 3.5rem;
|
||||
fill: var(--glass-strong-10);
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
<div
|
||||
v-for="photo in [
|
||||
...(coversInAlbum ? release.covers : []),
|
||||
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
||||
...release.photos,
|
||||
...release.caps,
|
||||
...(release.chapters?.map((chapter) => chapter.poster).filter(Boolean) || []),
|
||||
...(release.teaser?.mime.type === 'image' ? [release.poster] : []),
|
||||
]"
|
||||
:key="`photo-${photo.id}`"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<Campaign
|
||||
v-if="campaigns?.pagination"
|
||||
:campaign="campaigns.pagination"
|
||||
class="campaign-pagination"
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -301,6 +302,10 @@ function getPath(page) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
:deep(.campaign-pagination) .campaign-banner {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media(--small-10) {
|
||||
.campaign {
|
||||
padding: 0;
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
v-tooltip="'Duration'"
|
||||
class="chapter-duration"
|
||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
||||
|
||||
<time
|
||||
v-if="chapter.date"
|
||||
v-tooltip="formatDate(chapter.date, 'yyyy-MM-dd hh:mm')"
|
||||
:datetime="chapter.date"
|
||||
class="chapter-date"
|
||||
>{{ formatDate(chapter.date, 'MMM d') }}</time>
|
||||
</span>
|
||||
|
||||
<div class="chapter-info">
|
||||
@@ -87,7 +94,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDuration } from '#/utils/format.js';
|
||||
import { formatDuration, formatDate } from '#/utils/format.js';
|
||||
|
||||
const props = defineProps({
|
||||
chapters: {
|
||||
@@ -138,9 +145,9 @@ const timeline = computed(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
padding: 0 .75rem;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
margin: 0 0 .75rem 0;
|
||||
color: var(--text-light);
|
||||
background: var(--grey-dark-40);
|
||||
font-size: .8rem;
|
||||
@@ -164,7 +171,7 @@ const timeline = computed(() => {
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
padding: 0 .5rem;
|
||||
padding: 0 .75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -181,6 +188,7 @@ const timeline = computed(() => {
|
||||
}
|
||||
|
||||
.chapter-description {
|
||||
text-align: justify;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<Link
|
||||
:href="user?.abilities?.some((ability) => ability.plainUrls) ? scene.url : scene.watchUrl"
|
||||
:href="scene.watchUrl"
|
||||
:title="scene.date ? format(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${format(scene.createdAt, 'y-MM-dd')}`"
|
||||
target="_blank"
|
||||
class="date-link nolink"
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<TagsFilter
|
||||
:filters="filters"
|
||||
:tags="aggTags"
|
||||
:actor-tags="aggActorTags"
|
||||
@update="updateFilter"
|
||||
/>
|
||||
|
||||
@@ -65,6 +66,7 @@
|
||||
<Campaign
|
||||
v-if="campaigns?.meta"
|
||||
:campaign="campaigns.meta"
|
||||
class="campaign-meta"
|
||||
/>
|
||||
|
||||
<div class="views">
|
||||
@@ -155,6 +157,7 @@
|
||||
<Campaign
|
||||
v-if="campaigns?.scope"
|
||||
:campaign="campaigns.scope"
|
||||
class="campaign-scope"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
@@ -165,7 +168,10 @@
|
||||
v-if="item === 'campaign' && sceneCampaign"
|
||||
:key="`campaign-${item.id}`"
|
||||
>
|
||||
<Campaign :campaign="sceneCampaign" />
|
||||
<Campaign
|
||||
:campaign="sceneCampaign"
|
||||
:backdrop="true"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li
|
||||
@@ -258,6 +264,7 @@ const scenes = ref(pageProps.scenes);
|
||||
const aggYears = ref(pageProps.aggYears || []);
|
||||
const aggActors = ref(pageProps.aggActors || []);
|
||||
const aggTags = ref(pageProps.aggTags || []);
|
||||
const aggActorTags = ref(pageProps.aggActorTags || []);
|
||||
const aggChannels = ref(pageProps.aggChannels || []);
|
||||
|
||||
const currentPage = ref(Number(routeParams.page));
|
||||
@@ -358,6 +365,7 @@ async function search(options = {}) {
|
||||
aggYears.value = res.aggYears;
|
||||
aggActors.value = res.aggActors;
|
||||
aggTags.value = res.aggTags;
|
||||
aggActorTags.value = res.aggActorTags;
|
||||
aggChannels.value = res.aggChannels;
|
||||
|
||||
total.value = res.total;
|
||||
@@ -431,11 +439,11 @@ function setView(newView) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
gap: .5rem;
|
||||
padding: .5rem 1rem 1rem 1rem;
|
||||
}
|
||||
|
||||
:deep(.campaign) .campaign-banner {
|
||||
border-radius: .25rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||
}
|
||||
:deep(.campaign-meta) .campaign-banner,
|
||||
:deep(.campaign-scope) .campaign-banner {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.scopes {
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<ul
|
||||
class="row tags nolist"
|
||||
:title="scene.tags.map((tag) => tag.name).join(', ')"
|
||||
:title="sceneTags.map((tag) => tag.name).join(', ')"
|
||||
>
|
||||
<li
|
||||
v-if="scene.shootId"
|
||||
@@ -94,9 +94,10 @@
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="tag in scene.tags"
|
||||
v-for="tag in sceneTags"
|
||||
:key="`tag-${scene.id}-${tag.id}`"
|
||||
class="tag"
|
||||
:class="{ piss: tag.slug === 'pissing' }"
|
||||
>
|
||||
<Link
|
||||
:href="`/tag/${tag.slug}`"
|
||||
@@ -130,11 +131,14 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
const { user } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
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));
|
||||
</script>
|
||||
|
||||
@@ -263,6 +267,12 @@ const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id ===
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
/*
|
||||
.tag.piss {
|
||||
color: var(--text-piss);
|
||||
}
|
||||
*/
|
||||
|
||||
.shoot {
|
||||
font-size: .75rem;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -41,6 +41,7 @@ const cookies = Cookies.withConverter({
|
||||
const tags = {
|
||||
anal: 'anal',
|
||||
'anal-prolapse': 'anal prolapse',
|
||||
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||
pissing: 'pissing',
|
||||
gay: 'gay',
|
||||
transsexual: 'transsexual',
|
||||
|
||||
@@ -9,14 +9,13 @@
|
||||
v-for="photo in photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
:title="photo.comment"
|
||||
:href="`/img/${photo.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="photo-container"
|
||||
>
|
||||
<img
|
||||
:src="`/${photo.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/${photo.lazy})` }"
|
||||
:src="getPath(photo, 'thumbnail', { local: true })"
|
||||
:style="{ 'background-image': `url(${getPath(photo, 'lazy', { local: true })})` }"
|
||||
:alt="photo.comment"
|
||||
:width="photo.width"
|
||||
:height="photo.height"
|
||||
@@ -47,6 +46,8 @@ import { computed, inject } from 'vue';
|
||||
import Logo from '#/components/tags/logo.vue';
|
||||
import Campaign from '#/components/campaigns/campaign.vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: Object,
|
||||
|
||||
@@ -58,6 +58,91 @@ module.exports = {
|
||||
address: 'http://localhost:3000/script.js',
|
||||
siteId: '1b28ac3b-d229-43bf-aec9-75cf0a72a466',
|
||||
},
|
||||
restrictions: {
|
||||
enabled: false,
|
||||
modes: [
|
||||
null, // easier for 0 to mean disabled
|
||||
'block', // 1
|
||||
'censor', // 2
|
||||
],
|
||||
regions: {
|
||||
// Europe
|
||||
DE: 1, // Germany
|
||||
FR: 1, // France
|
||||
GB: 1, // Great Britain / United Kingdom
|
||||
IT: 1, // Italy
|
||||
// Asia & Oceania
|
||||
AU: 1, // Australia
|
||||
CN: 1, // China
|
||||
// Americas
|
||||
US: {
|
||||
AL: 1, // Alabama
|
||||
AR: 1, // Arkansas
|
||||
AZ: 1, // Arizona
|
||||
FL: 1, // Florida
|
||||
GA: 1, // Georgia
|
||||
ID: 1, // Idaho
|
||||
IN: 1, // Indiana
|
||||
KS: 1, // Kansas
|
||||
KY: 1, // Kentucky
|
||||
LA: 1, // Louisiana
|
||||
MO: 1, // Missouri
|
||||
MS: 1, // Mississippi
|
||||
MT: 1, // Montana
|
||||
NC: 1, // North Carolina
|
||||
ND: 1, // North Dakota
|
||||
NE: 1, // Nebraska
|
||||
OH: 1, // Ohio
|
||||
OK: 1, // Oklahoma
|
||||
SC: 1, // South Carolina
|
||||
SD: 1, // South Dakota
|
||||
TN: 1, // Tennessee
|
||||
TX: 1, // Texas
|
||||
UT: 1, // Utah
|
||||
VA: 1, // Virginia
|
||||
WY: 1, // Wyoming
|
||||
}, // only Florida
|
||||
},
|
||||
noVpn: [
|
||||
'AE', // United Arab Emirates
|
||||
'BY', // Belarus
|
||||
'CN', // China
|
||||
'IQ', // Iraq
|
||||
'IR', // Iran
|
||||
'KP', // North Korea
|
||||
'OM', // Oman
|
||||
'RU', // Russia
|
||||
'TM', // Turkmenistan
|
||||
'TR', // Turkey
|
||||
],
|
||||
censors: [ // additional to default filter
|
||||
'ball',
|
||||
'bisexual',
|
||||
'blow',
|
||||
'blowbang',
|
||||
'condom',
|
||||
'cowgirl',
|
||||
'creampie',
|
||||
'doggy',
|
||||
'facial',
|
||||
'finger',
|
||||
'gay',
|
||||
'hole',
|
||||
'horny',
|
||||
'lesbian',
|
||||
'masturbation',
|
||||
'milf',
|
||||
'missionary',
|
||||
'prolapse',
|
||||
'nymph',
|
||||
'sex',
|
||||
'swallowing',
|
||||
'squirt',
|
||||
'sucking',
|
||||
'threesome',
|
||||
'trans',
|
||||
],
|
||||
},
|
||||
auth: {
|
||||
login: true,
|
||||
signup: true,
|
||||
@@ -89,6 +174,7 @@ module.exports = {
|
||||
links: {
|
||||
content: 'mailto:content@traxxx.me',
|
||||
discord: 'https://discord.gg/gY6fnq6jJV',
|
||||
matrix: 'https://matrix.to/#/#traxxx:matrix.unknown.name',
|
||||
},
|
||||
stashes: {
|
||||
nameLength: [2, 24],
|
||||
@@ -101,7 +187,7 @@ module.exports = {
|
||||
},
|
||||
media: {
|
||||
path: './media',
|
||||
assetPath: '/img',
|
||||
assetPath: '',
|
||||
mediaPath: '/media',
|
||||
s3Path: 'https://s3.wasabisys.com',
|
||||
videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos
|
||||
|
||||
209
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "traxxx-web",
|
||||
"version": "0.45.10",
|
||||
"version": "0.47.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.45.10",
|
||||
"version": "0.47.12",
|
||||
"dependencies": {
|
||||
"@brillout/json-serializer": "^0.5.8",
|
||||
"@dicebear/collection": "^7.0.5",
|
||||
@@ -14,6 +14,7 @@
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@maxmind/geoip2-node": "^6.3.4",
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"@toycode/markdown-it-class": "^1.2.4",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
@@ -29,6 +30,7 @@
|
||||
"cron": "^3.1.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^3.0.0",
|
||||
"ejs": "^4.0.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -52,6 +54,7 @@
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"object.omit": "^3.0.0",
|
||||
"obscenity": "^0.4.6",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pg": "^8.11.3",
|
||||
"redis": "^4.6.12",
|
||||
@@ -3224,6 +3227,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@maxmind/geoip2-node": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.3.4.tgz",
|
||||
"integrity": "sha512-BTRFHCX7Uie4wVSPXsWQfg0EVl4eGZgLCts0BTKAP+Eiyt1zmF2UPyuUZkaj0R59XSDYO+84o1THAtaenUoQYg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"maxmind": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@modyfi/vite-plugin-yaml": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
||||
@@ -4527,9 +4539,10 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -5577,6 +5590,21 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
|
||||
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.9.1"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.18"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.616",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
||||
@@ -6790,6 +6818,36 @@
|
||||
"moment": "^2.29.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -8045,6 +8103,23 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
@@ -8510,6 +8585,20 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/maxmind": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
|
||||
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mmdb-lib": "3.0.2",
|
||||
"tiny-lru": "11.4.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -8653,6 +8742,16 @@
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"node_modules/mmdb-lib": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
|
||||
"integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
@@ -8991,6 +9090,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obscenity": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.6.tgz",
|
||||
"integrity": "sha512-pHk7kNN7j3L3zGhhGnwxjvXIGsPpLrcZl2r58fqWh/V/rH6b/dafscj2sMmAY+A/9/wPsocLmimgGk2DKeKsFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -10734,6 +10842,15 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"node_modules/tiny-lru": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
|
||||
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
||||
@@ -13920,6 +14037,14 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"@maxmind/geoip2-node": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-6.3.4.tgz",
|
||||
"integrity": "sha512-BTRFHCX7Uie4wVSPXsWQfg0EVl4eGZgLCts0BTKAP+Eiyt1zmF2UPyuUZkaj0R59XSDYO+84o1THAtaenUoQYg==",
|
||||
"requires": {
|
||||
"maxmind": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"@modyfi/vite-plugin-yaml": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.0.tgz",
|
||||
@@ -14759,9 +14884,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -15518,6 +15643,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"ejs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz",
|
||||
"integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==",
|
||||
"requires": {
|
||||
"jake": "^10.9.1"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.616",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
|
||||
@@ -16434,6 +16567,32 @@
|
||||
"moment": "^2.29.1"
|
||||
}
|
||||
},
|
||||
"filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"requires": {
|
||||
"minimatch": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -17277,6 +17436,16 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"jake": {
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"requires": {
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"picocolors": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
@@ -17637,6 +17806,15 @@
|
||||
"typed-function": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"maxmind": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz",
|
||||
"integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==",
|
||||
"requires": {
|
||||
"mmdb-lib": "3.0.2",
|
||||
"tiny-lru": "11.4.7"
|
||||
}
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -17738,6 +17916,11 @@
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"mmdb-lib": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
|
||||
"integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
@@ -17989,6 +18172,11 @@
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"obscenity": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/obscenity/-/obscenity-0.4.6.tgz",
|
||||
"integrity": "sha512-pHk7kNN7j3L3zGhhGnwxjvXIGsPpLrcZl2r58fqWh/V/rH6b/dafscj2sMmAY+A/9/wPsocLmimgGk2DKeKsFQ=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -19227,6 +19415,11 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"tiny-lru": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
|
||||
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "npm run server:dev",
|
||||
"dev": "node ./src/app",
|
||||
"prod": "npm run build && npm run server:prod",
|
||||
"build": "vite build",
|
||||
"server:dev": "node ./src/app",
|
||||
@@ -14,6 +14,7 @@
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@maxmind/geoip2-node": "^6.3.4",
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"@toycode/markdown-it-class": "^1.2.4",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
@@ -29,6 +30,7 @@
|
||||
"cron": "^3.1.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^3.0.0",
|
||||
"ejs": "^4.0.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -52,6 +54,7 @@
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"object.omit": "^3.0.0",
|
||||
"obscenity": "^0.4.6",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pg": "^8.11.3",
|
||||
"redis": "^4.6.12",
|
||||
@@ -89,7 +92,7 @@
|
||||
"overrides": {
|
||||
"vite": "$vite"
|
||||
},
|
||||
"version": "0.45.10",
|
||||
"version": "0.47.12",
|
||||
"imports": {
|
||||
"#/*": "./*.js"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,16 @@
|
||||
class="row"
|
||||
>
|
||||
<div class="item-header">
|
||||
<div class="key">{{ item.label || item.key }}</div>
|
||||
<div class="key">
|
||||
{{ item.label || item.key }}
|
||||
|
||||
<Icon
|
||||
v-if="item.note"
|
||||
v-tooltip="item.note"
|
||||
icon="info2"
|
||||
class="item-note"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item-actions noselect">
|
||||
<Icon
|
||||
@@ -443,6 +452,7 @@ const fields = computed(() => [
|
||||
{
|
||||
key: 'augmentation',
|
||||
type: 'augmentation',
|
||||
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
||||
value: {
|
||||
naturalBoobs: actor.value.naturalBoobs,
|
||||
boobsVolume: actor.value.boobsVolume,
|
||||
@@ -503,6 +513,7 @@ const fields = computed(() => [
|
||||
{
|
||||
key: 'piercings',
|
||||
type: 'has',
|
||||
note: 'Excludes earrings',
|
||||
value: {
|
||||
has: actor.value.hasPiercings,
|
||||
description: actor.value.piercings,
|
||||
@@ -685,10 +696,22 @@ async function submit() {
|
||||
|
||||
.key {
|
||||
width: 10rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-note{
|
||||
fill: var(--glass);
|
||||
padding: .5rem .75rem;
|
||||
cursor: help;
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ const popularNetworks = [
|
||||
'elegantangel',
|
||||
'evilangel',
|
||||
'fakehub',
|
||||
'hentaied',
|
||||
'hookuphotshot',
|
||||
'hussiepass',
|
||||
'julesjordan',
|
||||
|
||||
@@ -3,7 +3,9 @@ import { fetchEntities } from '#/src/entities.js';
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const networks = await fetchEntities(pageContext.urlParsed.search.q
|
||||
? { query: pageContext.urlParsed.search.q }
|
||||
: { type: 'primary' });
|
||||
: { type: 'primary' }, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="user?.abilities.some((ability) => ability.plainUrls)"
|
||||
v-if="user?.abilities?.some((ability) => ability.plainUrls)"
|
||||
:href="entity.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
|
||||
@@ -20,7 +20,9 @@ async function fetchReleases(pageContext, entityId) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchScenes(await curateScenesQuery({
|
||||
@@ -32,7 +34,9 @@ async function fetchReleases(pageContext, entityId) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
@@ -47,7 +51,9 @@ export async function onBeforeRender(pageContext) {
|
||||
[entity],
|
||||
entityReleases,
|
||||
] = await Promise.all([
|
||||
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user),
|
||||
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
fetchReleases(pageContext, entityId),
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ export async function onBeforeRender(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 50,
|
||||
dedupe: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
|
||||
@@ -18,7 +18,7 @@ function getTitle(movie) {
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [[movie], movieScenes] = await Promise.all([
|
||||
fetchMoviesById([Number(pageContext.routeParams.movieId)], pageContext.user),
|
||||
fetchMoviesById([Number(pageContext.routeParams.movieId)], pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchScenes(await curateScenesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: 'oldest',
|
||||
@@ -27,7 +27,9 @@ export async function onBeforeRender(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!movie) {
|
||||
|
||||
@@ -140,20 +140,30 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
v-if="scene.tags.length > 0"
|
||||
class="tags nolist"
|
||||
>
|
||||
<li
|
||||
v-for="tag in scene.tags"
|
||||
:key="`tag-${tag.id}`"
|
||||
<div class="tags">
|
||||
<div
|
||||
v-for="actorTags in tags"
|
||||
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||
class="tags-section"
|
||||
>
|
||||
<Link
|
||||
:href="`/tag/${tag.slug}`"
|
||||
class="tag nolink"
|
||||
>{{ tag.name }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-if="actorTags.actor"
|
||||
class="tags-actor"
|
||||
>{{ actorTags.actor.name }}:</li>
|
||||
|
||||
<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
|
||||
v-if="scene.movies.length > 0 || scene.series.length > 0"
|
||||
@@ -249,12 +259,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chapters
|
||||
v-if="scene.chapters.length > 0"
|
||||
:chapters="scene.chapters"
|
||||
class="section"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="scene.description"
|
||||
class="section"
|
||||
@@ -264,6 +268,18 @@
|
||||
<p class="description">{{ scene.description }}</p>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="scene.chapters.length > 0"
|
||||
class="section"
|
||||
>
|
||||
<h3 class="heading">Chapters</h3>
|
||||
|
||||
<Chapters
|
||||
:chapters="scene.chapters"
|
||||
class="section"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="campaigns?.scene"
|
||||
class="section"
|
||||
@@ -434,6 +450,17 @@ const {
|
||||
|
||||
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 qualities = {
|
||||
@@ -634,6 +661,22 @@ function copySummary() {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -25,6 +25,7 @@ export async function onBeforeRender(pageContext) {
|
||||
includeAssets: true,
|
||||
includePartOf: true,
|
||||
actorStashes: true,
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
const [campaigns, tagIds] = await Promise.all([
|
||||
|
||||
@@ -66,7 +66,16 @@
|
||||
class="row"
|
||||
>
|
||||
<div class="item-header">
|
||||
<div class="key">{{ item.label || item.key }}</div>
|
||||
<div class="key">
|
||||
{{ item.label || item.key }}
|
||||
|
||||
<Icon
|
||||
v-if="item.note"
|
||||
v-tooltip="item.note"
|
||||
icon="info2"
|
||||
class="item-note"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item-actions">
|
||||
<Icon
|
||||
@@ -252,6 +261,8 @@ const fields = computed(() => [
|
||||
key: 'tags',
|
||||
type: 'tags',
|
||||
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',
|
||||
@@ -262,11 +273,13 @@ const fields = computed(() => [
|
||||
key: 'title',
|
||||
type: 'string',
|
||||
value: scene.value.title,
|
||||
note: 'Do not correct language errors unless source was updated.',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
type: 'text',
|
||||
value: scene.value.description,
|
||||
note: 'Do not correct language errors unless source was updated.',
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
@@ -408,6 +421,8 @@ async function submit() {
|
||||
|
||||
.key {
|
||||
width: 8rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -17,7 +17,9 @@ export async function onBeforeRender(pageContext) {
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
||||
aggregate: true,
|
||||
dedupe: true,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
getRandomCampaigns([
|
||||
{ minRatio: 0.75, maxRatio: 1.25 },
|
||||
{ minRatio: 1.5 },
|
||||
|
||||
@@ -16,22 +16,24 @@ export async function onBeforeRender(pageContext) {
|
||||
}), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 15,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchActors(curateActorsQuery(pageContext.urlQuery), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 10,
|
||||
order: ['results', 'desc'],
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchMovies(await curateMoviesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: pageContext.routeParams.scope || 'results',
|
||||
}), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 5,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchEntities({
|
||||
query: pageContext.urlParsed.search.q,
|
||||
limit: 5,
|
||||
}, {
|
||||
restriction: pageContext.restriction,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
>
|
||||
<img
|
||||
v-if="tag.poster"
|
||||
:src="`/${tag.poster.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/${tag.poster.lazy})` }"
|
||||
:src="getPath(tag.poster, 'thumbnail', { local: true })"
|
||||
:style="{ 'background-image': `url(${getPath(tag.poster, 'lazy', { local: true })})` }"
|
||||
:title="tag.poster.comment"
|
||||
class="thumb"
|
||||
loading="lazy"
|
||||
@@ -111,6 +111,7 @@ import { ref, onMounted, inject } from 'vue';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
import events from '#/src/events.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import Logo from '#/components/tags/logo.vue';
|
||||
|
||||
@@ -324,6 +325,7 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 5/3;
|
||||
border-radius: .25rem;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchTags, fetchTagsById } from '#/src/tags.js';
|
||||
import { censor } from '#/src/censor.js';
|
||||
|
||||
const tagSlugs = {
|
||||
popular: [
|
||||
@@ -117,7 +118,7 @@ const tagSlugs = {
|
||||
};
|
||||
|
||||
async function searchTags(pageContext) {
|
||||
const tags = await fetchTags({ query: pageContext.urlParsed.search.q });
|
||||
const tags = await fetchTags({ query: pageContext.urlParsed.search.q }, { restriction: pageContext.restriction });
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
@@ -136,13 +137,13 @@ export async function onBeforeRender(pageContext) {
|
||||
return searchTags(pageContext);
|
||||
}
|
||||
|
||||
const tags = await fetchTagsById(Object.values(tagSlugs).flat());
|
||||
const tags = await fetchTagsById(Object.values(tagSlugs).flat(), {}, pageContext.user, { restriction: pageContext.restriction });
|
||||
|
||||
const filteredTags = tags.filter((tag) => !pageContext.tagFilter.includes(tag.name) && !pageContext.tagFilter.includes(tag.slug));
|
||||
const tagsBySlug = Object.fromEntries(filteredTags.map((tag) => [tag.slug, tag]));
|
||||
|
||||
const tagShowcase = Object.fromEntries(Object.entries(tagSlugs).map(([category, categorySlugs]) => [
|
||||
category,
|
||||
censor(category, pageContext.restriction),
|
||||
categorySlugs.map((slug) => tagsBySlug[slug]).filter(Boolean),
|
||||
]));
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ async function fetchReleases(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchScenes(await curateScenesQuery({
|
||||
@@ -33,14 +35,16 @@ async function fetchReleases(pageContext) {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}, pageContext.user);
|
||||
}, pageContext.user, {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
}
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const tagSlug = pageContext.routeParams.tagSlug;
|
||||
|
||||
const [[tag], tagReleases, campaigns] = await Promise.all([
|
||||
fetchTagsById([tagSlug], {}, pageContext.user),
|
||||
fetchTagsById([tagSlug], {}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
fetchReleases(pageContext),
|
||||
getRandomCampaigns([
|
||||
{ tagSlugs: [tagSlug], minRatio: 0.75, maxRatio: 1.25 },
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function onBeforeRender(pageContext) {
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 29,
|
||||
aggregate: withQuery,
|
||||
dedupe: true,
|
||||
}, pageContext.user),
|
||||
}, pageContext.user, { restriction: pageContext.restriction }),
|
||||
getRandomCampaigns([
|
||||
{ minRatio: 2.0, maxRatio: 5 },
|
||||
{ minRatio: 0.75, maxRatio: 1.25 },
|
||||
|
||||
@@ -9,5 +9,6 @@ export default {
|
||||
'assets',
|
||||
'campaigns',
|
||||
'meta',
|
||||
'restriction',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -122,7 +122,10 @@ export function curateActor(actor, context = {}) {
|
||||
state: actor.residence_state,
|
||||
},
|
||||
agency: actor.agency,
|
||||
avatar: curateMedia(actor.avatar),
|
||||
avatar: actor.avatar && curateMedia({
|
||||
...actor.avatar,
|
||||
sfw_media: actor.sfw_avatar,
|
||||
}),
|
||||
socials: context.socials?.map((social) => ({
|
||||
id: social.id,
|
||||
url: social.url,
|
||||
@@ -205,26 +208,33 @@ export function sortActorsByGender(actors, context = {}) {
|
||||
export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
const [actors, profiles, photos, socials, stashes, alerts] = await Promise.all([
|
||||
knex('actors')
|
||||
.select(
|
||||
'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')
|
||||
.select('actors.*')
|
||||
.whereIn('actors.id', actorIds)
|
||||
.modify((builder) => {
|
||||
if (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')
|
||||
.select(
|
||||
@@ -245,10 +255,12 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
'media.*',
|
||||
'actors_avatars.actor_id',
|
||||
knex.raw('json_agg(actors_avatars.profile_id) as profile_ids'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_media'),
|
||||
)
|
||||
.whereIn('actor_id', actorIds)
|
||||
.leftJoin('media', 'media.id', 'actors_avatars.media_id')
|
||||
.groupBy('media.id', 'actors_avatars.actor_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'sfw_media.id', 'actors_avatars.actor_id')
|
||||
.orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'),
|
||||
knex('actors_socials')
|
||||
.whereIn('actor_id', actorIds),
|
||||
@@ -263,12 +275,15 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
.where('user_id', reqUser.id)
|
||||
.whereIn('actor_id', actorIds)
|
||||
: [],
|
||||
]);
|
||||
].slice(0, options.shallow ? 1 : -1));
|
||||
|
||||
if (options.order) {
|
||||
return actors.map((actorEntry) => curateActor(actorEntry, {
|
||||
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
|
||||
alerts: alerts.filter((alert) => alert.actor_id === actorEntry.id),
|
||||
stashes: stashes?.filter((stash) => stash.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,
|
||||
}));
|
||||
}
|
||||
@@ -282,11 +297,11 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
}
|
||||
|
||||
return curateActor(actor, {
|
||||
stashes: stashes.filter((stash) => stash.actor_id === actor.id),
|
||||
alerts: alerts.filter((alert) => alert.actor_id === actor.id),
|
||||
profiles: profiles.filter((profile) => profile.actor_id === actor.id),
|
||||
photos: photos.filter((photo) => photo.actor_id === actor.id),
|
||||
socials: socials.filter((social) => social.actor_id === actor.id),
|
||||
stashes: stashes?.filter((stash) => stash.actor_id === actor.id),
|
||||
alerts: alerts?.filter((alert) => alert.actor_id === actor.id),
|
||||
profiles: profiles?.filter((profile) => profile.actor_id === actor.id),
|
||||
photos: photos?.filter((photo) => photo.actor_id === actor.id),
|
||||
socials: socials?.filter((social) => social.actor_id === actor.id),
|
||||
append: options.append,
|
||||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
@@ -31,13 +31,17 @@ export function getAffiliateSceneUrl(scene) {
|
||||
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
|
||||
? scenePath
|
||||
: scenePath.replace(/^\//, ''),
|
||||
scenePath: scene.affiliate.parameters.prefixSlash === false
|
||||
? scenePath.replace(/^\//, '')
|
||||
: scenePath,
|
||||
entryId: scene.entryId,
|
||||
});
|
||||
}
|
||||
@@ -65,7 +69,7 @@ export function getAffiliateSceneUrl(scene) {
|
||||
&& (!scene.channel.isIndependent || scene.channel.id === scene.affiliate.entityId)) {
|
||||
const { pathname, search } = new URL(watchUrl);
|
||||
|
||||
return `${affiliateUrl}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites
|
||||
return `${affiliateUrl}${pathname.replace(/^\/(trial|tour)/, '')}${search}`; // replace needed for Jules Jordan and HussiePass, verify behavior on other sites
|
||||
}
|
||||
|
||||
const affiliateUrlComponents = new URL(affiliateUrl);
|
||||
@@ -101,7 +105,15 @@ export function getAffiliateEntityUrl(entity, affiliate) {
|
||||
return entityUrl;
|
||||
}
|
||||
|
||||
if (entityAffiliate.parameters?.query) { // used by e.g. Bang
|
||||
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)),
|
||||
@@ -110,14 +122,6 @@ export function getAffiliateEntityUrl(entity, affiliate) {
|
||||
return `${entityUrl}?${newParams.toString()}`;
|
||||
}
|
||||
|
||||
const affiliateUrl = entityAffiliate.parameters?.replaceEntity?.hostname === new URL(entityUrl).hostname
|
||||
? entityAffiliate.parameters.replaceEntity.url
|
||||
: entityAffiliate.url;
|
||||
|
||||
if (entity.id === entityAffiliate.entityId || entityUrl === entity.parent?.url) {
|
||||
return affiliateUrl;
|
||||
}
|
||||
|
||||
if (entity.type === 'network' || entity.isIndependent) {
|
||||
return entityUrl;
|
||||
}
|
||||
|
||||
10
src/app.js
@@ -1,14 +1,8 @@
|
||||
import initServer from './web/server.js';
|
||||
import { cacheTagIds } from './tags.js';
|
||||
import { cacheEntityIds } from './entities.js';
|
||||
import { cacheCampaigns } from './campaigns.js';
|
||||
import { initCaches } from './cache.js';
|
||||
|
||||
async function init() {
|
||||
await Promise.all([
|
||||
cacheTagIds(),
|
||||
cacheEntityIds(),
|
||||
cacheCampaigns(),
|
||||
]);
|
||||
await initCaches();
|
||||
|
||||
initServer();
|
||||
}
|
||||
|
||||
10
src/argv.js
@@ -1,11 +1,15 @@
|
||||
import yargs from 'yargs';
|
||||
|
||||
const { argv } = yargs()
|
||||
.command('npm start')
|
||||
const { argv } = yargs(process.argv.slice(2))
|
||||
.option('debug', {
|
||||
describe: 'Show error stack traces',
|
||||
describe: 'Show error stack traces and inputs',
|
||||
type: 'boolean',
|
||||
default: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
.option('ip', {
|
||||
describe: 'Mock IP address',
|
||||
type: 'string',
|
||||
default: null,
|
||||
});
|
||||
|
||||
export default argv;
|
||||
|
||||
12
src/cache.js
@@ -1,5 +1,9 @@
|
||||
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) {
|
||||
if (!slugs) {
|
||||
return [];
|
||||
@@ -25,3 +29,11 @@ export async function getIdsBySlug(slugs, domain, toMap) {
|
||||
|
||||
return ids.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function initCaches() {
|
||||
await Promise.all([
|
||||
cacheTagIds(),
|
||||
cacheEntityIds(),
|
||||
cacheCampaigns(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getAffiliateEntityUrl } from './affiliates.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
function getCampaignUrl(campaign) {
|
||||
function getCampaignUrl(campaign, entity) {
|
||||
if (!campaign) {
|
||||
return null;
|
||||
}
|
||||
@@ -17,8 +17,13 @@ function getCampaignUrl(campaign) {
|
||||
return campaign.url;
|
||||
}
|
||||
|
||||
if (campaign.affiliate?.url) {
|
||||
return campaign.affiliate.url;
|
||||
}
|
||||
|
||||
if (campaign.entity) {
|
||||
return getAffiliateEntityUrl(campaign.entity, campaign.affiliate);
|
||||
// resolve e.g. parameter tracking
|
||||
return getAffiliateEntityUrl(entity, campaign.affiliate);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -29,9 +34,11 @@ function curateCampaign(campaign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entity = campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity });
|
||||
|
||||
const curatedCampaign = {
|
||||
id: campaign.id,
|
||||
entity: campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity }),
|
||||
entity,
|
||||
banner: campaign.banner && {
|
||||
id: campaign.banner.id,
|
||||
type: campaign.banner.type,
|
||||
@@ -48,7 +55,7 @@ function curateCampaign(campaign) {
|
||||
},
|
||||
};
|
||||
|
||||
curatedCampaign.url = getCampaignUrl(curatedCampaign);
|
||||
curatedCampaign.url = getCampaignUrl(campaign, entity);
|
||||
|
||||
return curatedCampaign;
|
||||
}
|
||||
@@ -75,18 +82,27 @@ export async function getRandomCampaign(options = {}, context = {}, pass = 0) {
|
||||
|
||||
const validCampaigns = campaigns.filter((campaign) => {
|
||||
if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) {
|
||||
// too small
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) {
|
||||
// too big
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) {
|
||||
// this is an entity page, this campaign does not belong to this entity
|
||||
return false;
|
||||
}
|
||||
|
||||
if (campaign.affiliate?.parameters?.global === false && !options.entityIds) {
|
||||
// this campaign should only show on entity page
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag) && !options.tagSlugs?.includes(tag))) {
|
||||
// wrong tag
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -3,29 +3,36 @@ import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import entityPrefixes from './entities-prefixes.js';
|
||||
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
export function curateEntity(entity, context) {
|
||||
export function curateEntity(entity, context = {}) {
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedEntity = {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
name: censor(entity.name, context.restriction),
|
||||
slug: entity.slug,
|
||||
type: entity.type,
|
||||
url: entity.url,
|
||||
isIndependent: entity.independent,
|
||||
hasLogo: entity.has_logo,
|
||||
hasLogo: context.restriction ? false : entity.has_logo,
|
||||
parent: curateEntity(entity.parent, context),
|
||||
tags: context?.tags?.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
})),
|
||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({ ...child, parent: entity }, { parent: entity })) || [],
|
||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({
|
||||
...child,
|
||||
parent: entity,
|
||||
}, {
|
||||
parent: entity,
|
||||
restriction: context.restriction,
|
||||
})) || [],
|
||||
affiliate: entity.affiliate ? {
|
||||
id: entity.affiliate.id,
|
||||
entityId: entity.affiliate.entity_id,
|
||||
@@ -44,7 +51,7 @@ export function curateEntity(entity, context) {
|
||||
return curatedEntity;
|
||||
}
|
||||
|
||||
export async function fetchEntities(options = {}) {
|
||||
export async function fetchEntities(options = {}, context = {}) {
|
||||
const entities = await knex('entities')
|
||||
.select('entities.*', knex.raw('row_to_json(parents) as parent'))
|
||||
.modify((builder) => {
|
||||
@@ -93,11 +100,12 @@ export async function fetchEntities(options = {}) {
|
||||
.whereIn('entity_id', entities.map((entity) => entity.id));
|
||||
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||
...context,
|
||||
tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser, context) {
|
||||
const [entities, children, tags, alerts] = await Promise.all([
|
||||
knex('entities')
|
||||
.select(
|
||||
@@ -136,6 +144,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
|
||||
if (options.order) {
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||
...context,
|
||||
append: options.append,
|
||||
children: children.filter((channel) => channel.parent_id === entityEntry.id),
|
||||
alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id),
|
||||
@@ -151,6 +160,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
}
|
||||
|
||||
return curateEntity(entity, {
|
||||
...context,
|
||||
append: options.append,
|
||||
children: children.filter((channel) => channel.parent_id === entity.id),
|
||||
tags: tags.filter((tag) => tag.entity_id === entity.id),
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// import config from 'config';
|
||||
import { pageContext } from '../renderer/usePageContext.js';
|
||||
|
||||
function getBasePath(media, type, options) {
|
||||
/*
|
||||
if (store.state.ui.sfw) {
|
||||
return config.media.assetPath;
|
||||
function getBasePath(media, options) {
|
||||
if (pageContext.restriction) {
|
||||
return pageContext.env.media.assetPath;
|
||||
}
|
||||
*/
|
||||
|
||||
if (media.isS3) {
|
||||
return options.s3Path;
|
||||
@@ -20,15 +17,13 @@ function getBasePath(media, type, options) {
|
||||
}
|
||||
|
||||
function getFilename(media, type, options) {
|
||||
/*
|
||||
if (store.state.ui.sfw && type && !options?.original) {
|
||||
return media.sfw[type];
|
||||
if (pageContext.restriction && type && !options?.original) {
|
||||
return media.sfw?.[type];
|
||||
}
|
||||
|
||||
if (store.state.ui.sfw) {
|
||||
return media.sfw.path;
|
||||
if (pageContext.restriction) {
|
||||
return media.sfw?.path;
|
||||
}
|
||||
*/
|
||||
|
||||
if (type && !options?.original) {
|
||||
return media[type];
|
||||
@@ -42,7 +37,7 @@ export default function getPath(media, type, options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = getBasePath(media, type, { ...pageContext.env.media, ...options });
|
||||
const path = getBasePath(media, { ...pageContext.env.media, ...options });
|
||||
const filename = getFilename(media, type, { ...pageContext.env.media, ...options });
|
||||
|
||||
return `${path}/${filename}`;
|
||||
|
||||
@@ -30,6 +30,7 @@ export function curateMedia(media, context = {}) {
|
||||
parent: media.entity_parent,
|
||||
}),
|
||||
type: context.type || null,
|
||||
sfw: curateMedia(media.sfw_media),
|
||||
isRestricted: context.isRestricted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,29 +8,30 @@ import { curateMedia } from './media.js';
|
||||
import { fetchTagsById } from './tags.js';
|
||||
import { fetchEntitiesById } from './entities.js';
|
||||
import { curateStash } from './stashes.js';
|
||||
import { censor } from './censor.js';
|
||||
import escape from '../utils/escape-manticore.js';
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
|
||||
function curateMovie(rawMovie, assets) {
|
||||
function curateMovie(rawMovie, assets, context = {}) {
|
||||
if (!rawMovie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: rawMovie.id,
|
||||
title: rawMovie.title,
|
||||
title: censor(rawMovie.title, context.restriction),
|
||||
slug: rawMovie.slug,
|
||||
url: rawMovie.url,
|
||||
date: rawMovie.date,
|
||||
datePrecision: rawMovie.date_precision,
|
||||
createdAt: rawMovie.created_at,
|
||||
effectiveDate: rawMovie.effective_date,
|
||||
description: rawMovie.description,
|
||||
description: censor(rawMovie.description, context.restriction),
|
||||
duration: rawMovie.duration,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
@@ -38,7 +39,7 @@ function curateMovie(rawMovie, assets) {
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
} : null,
|
||||
@@ -51,7 +52,7 @@ function curateMovie(rawMovie, assets) {
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
})),
|
||||
// poster: curateMedia(assets.poster),
|
||||
covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })),
|
||||
@@ -64,7 +65,7 @@ function curateMovie(rawMovie, assets) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMoviesById(movieIds, reqUser) {
|
||||
export async function fetchMoviesById(movieIds, reqUser, context = {}) {
|
||||
const {
|
||||
movies,
|
||||
channels,
|
||||
@@ -123,20 +124,25 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||
.orderBy('priority', 'desc'),
|
||||
covers: knex('movies_covers')
|
||||
.select('media.*', 'movies_covers.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movie_id', movieIds)
|
||||
.leftJoin('media', 'media.id', 'movies_covers.media_id')
|
||||
.orderBy('media.index'),
|
||||
photos: knex.transaction(async (trx) => {
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.orderBy('media.index')
|
||||
.groupBy('media.id', 'movies_covers.movie_id', 'sfw_media.id'),
|
||||
photos: context.restriction ? [] : knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('movies_scenes')
|
||||
.select('media.*', 'movies_scenes.movie_id')
|
||||
.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movies_scenes.movie_id', movieIds)
|
||||
.whereNotNull('media.id')
|
||||
.leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id')
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id');
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
|
||||
}),
|
||||
caps: knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -144,11 +150,13 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
}
|
||||
|
||||
return trx('movies_scenes')
|
||||
.select('media.*', 'movies_scenes.movie_id')
|
||||
.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movies_scenes.movie_id', movieIds)
|
||||
.whereNotNull('media.id')
|
||||
.leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_id')
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id');
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
|
||||
}),
|
||||
trailers: knex('movies_trailers')
|
||||
.whereIn('movie_id', movieIds)
|
||||
@@ -192,7 +200,7 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
caps: movieCaps,
|
||||
trailer: movieTrailer,
|
||||
stashes: movieStashes,
|
||||
});
|
||||
}, context);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -398,7 +406,7 @@ function countAggregations(buckets) {
|
||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
}
|
||||
|
||||
export async function fetchMovies(filters, rawOptions, reqUser) {
|
||||
export async function fetchMovies(filters, rawOptions, reqUser, context) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
console.log(options);
|
||||
@@ -413,13 +421,13 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
|
||||
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
||||
|
||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [],
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { 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 }, reqUser, context) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||
]);
|
||||
|
||||
const movieIds = result.movies.map((movie) => Number(movie.id));
|
||||
const movies = await fetchMoviesById(movieIds, reqUser);
|
||||
const movies = await fetchMoviesById(movieIds, reqUser, context);
|
||||
|
||||
return {
|
||||
movies,
|
||||
|
||||
160
src/scenes.js
@@ -1,6 +1,7 @@
|
||||
import config from 'config';
|
||||
import { MerkleJson } from 'merkle-json';
|
||||
|
||||
import argv from './argv.js';
|
||||
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
|
||||
import { utilsApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
@@ -15,18 +16,19 @@ import promiseProps from '../utils/promise-props.js';
|
||||
import initLogger from './logger.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { getAffiliateSceneUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
|
||||
function curateScene(rawScene, assets, reqUser) {
|
||||
function curateScene(rawScene, assets, reqUser, context) {
|
||||
if (!rawScene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedScene = {
|
||||
id: rawScene.id,
|
||||
title: rawScene.title,
|
||||
title: censor(rawScene.title, context.restriction),
|
||||
slug: rawScene.slug,
|
||||
url: rawScene.url,
|
||||
entryId: rawScene.entry_id,
|
||||
@@ -34,14 +36,14 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
datePrecision: rawScene.date_precision,
|
||||
createdAt: rawScene.created_at,
|
||||
effectiveDate: rawScene.effective_date,
|
||||
description: rawScene.description,
|
||||
description: censor(rawScene.description, context.restriction),
|
||||
duration: rawScene.duration,
|
||||
shootId: rawScene.shoot_id,
|
||||
productionDate: rawScene.production_date,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
@@ -49,7 +51,7 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.network_has_logo,
|
||||
} : null,
|
||||
@@ -78,15 +80,20 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
priority: tag.priority,
|
||||
actorId: tag.actor_id,
|
||||
})),
|
||||
chapters: assets.chapters.map((chapter) => ({
|
||||
id: chapter.id,
|
||||
title: chapter.title,
|
||||
description: chapter.description,
|
||||
time: chapter.time,
|
||||
date: chapter.date,
|
||||
duration: chapter.duration,
|
||||
poster: curateMedia(chapter.chapter_poster),
|
||||
poster: context.restriction
|
||||
? null
|
||||
: curateMedia(chapter.chapter_poster, { type: 'poster' }),
|
||||
tags: chapter.chapter_tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
@@ -98,14 +105,18 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
movies: assets.movies.map((movie) => ({
|
||||
id: movie.id,
|
||||
slug: movie.slug,
|
||||
title: movie.title,
|
||||
covers: movie.movie_covers?.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index) || [],
|
||||
title: censor(movie.title, context.restriction),
|
||||
covers: movie.movie_covers && !context.restriction
|
||||
? movie.movie_covers.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index)
|
||||
: [],
|
||||
})),
|
||||
series: assets.series.map((serie) => ({
|
||||
id: serie.id,
|
||||
slug: serie.slug,
|
||||
title: serie.title,
|
||||
poster: curateMedia(serie.serie_poster, { type: 'poster' }),
|
||||
poster: context.restriction
|
||||
? null
|
||||
: (serie.serie_poster, { type: 'poster' }),
|
||||
})),
|
||||
poster: curateMedia(assets.poster, { type: 'poster' }),
|
||||
photos: assets.photos?.map((photo) => curateMedia(photo, { type: 'photo' })) || [],
|
||||
@@ -127,7 +138,11 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
|
||||
|
||||
if (!isVideoRestricted || reqUser?.abilities?.some((ability) => ability.trailerAccess)) {
|
||||
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted });
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -189,19 +204,22 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.select(
|
||||
'actors.*',
|
||||
knex.raw('row_to_json(avatars) as avatar'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||
'countries.name as birth_country_name',
|
||||
'countries.alias as birth_country_alias',
|
||||
'releases_actors.release_id',
|
||||
)
|
||||
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||
.leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2')
|
||||
.whereIn('release_id', sceneIds),
|
||||
.whereIn('release_id', sceneIds)
|
||||
.groupBy('actors.id', 'releases_actors.release_id', 'avatars.id', 'countries.name', 'countries.alias', 'sfw_media.id'),
|
||||
directors: knex('releases_directors')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||
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')
|
||||
.whereNotNull('tags.id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
@@ -234,17 +252,23 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.whereIn('scene_id', sceneIds)
|
||||
.groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [],
|
||||
posters: knex('releases_posters')
|
||||
.select('media.*', 'releases_posters.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
||||
photos: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'releases_posters.release_id', 'sfw_media.id'),
|
||||
photos: context.includeAssets && !context.restriction ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('releases_photos')
|
||||
.select('media.*', 'releases_photos.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('index');
|
||||
.orderBy('index')
|
||||
.groupBy('media.id', 'releases_photos.release_id', 'sfw_media.id');
|
||||
}) : [],
|
||||
caps: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -252,9 +276,12 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
}
|
||||
|
||||
return trx('releases_caps')
|
||||
.select('media.*', 'releases_caps.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('index');
|
||||
.orderBy('index')
|
||||
.groupBy('media.id', 'releases_caps.release_id', 'sfw_media.id');
|
||||
}) : [],
|
||||
trailers: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -350,7 +377,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
stashes: sceneStashes,
|
||||
actorStashes: sceneActorStashes,
|
||||
lastBatchId: lastBatch?.id,
|
||||
}, reqUser);
|
||||
}, reqUser, context);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -375,6 +402,14 @@ function curateOptions(options) {
|
||||
};
|
||||
}
|
||||
|
||||
function curateFacet(results, field, count = 'count(distinct id)') {
|
||||
return results
|
||||
.find((result) => result.columns[0][field] && result.columns[1][count])
|
||||
?.data.map((row) => ({ key: row[field], doc_count: row[count] }))
|
||||
.filter((row) => !!row.key)
|
||||
|| [];
|
||||
}
|
||||
|
||||
async function queryManticoreSql(filters, options, _reqUser) {
|
||||
const aggSize = config.database.manticore.maxAggregateSize;
|
||||
|
||||
@@ -429,6 +464,13 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
year(scenes.effective_date) as effective_year,
|
||||
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) {
|
||||
@@ -531,11 +573,17 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
.limit(options.limit)
|
||||
.offset((options.page - 1) * options.limit),
|
||||
// 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,
|
||||
actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids order by count(*) desc limit ?', [aggSize]) : null,
|
||||
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids order by count(*) desc limit ?', [aggSize]) : null,
|
||||
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id order by count(*) desc limit ?', [aggSize]) : null,
|
||||
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id order by count(*) 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 as actors_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
|
||||
// don't facet tags associated to other actors, actor ID 0 means global
|
||||
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids as tags_facet order by count(distinct id) desc limit ?', [aggSize]) : null,
|
||||
/*
|
||||
actorTagsFacet: options.aggregateTags && !filters.stashId // eslint-disable-line no-nested-ternary
|
||||
? knex.raw(`facet IF(IN(scenes_tags.actor_id, ${[0, ...filters?.actorIds || []]}), scenes_tags.tag_id, 0) as actor_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,
|
||||
maxQueryTime: config.database.manticore.maxQueryTime,
|
||||
}).toString();
|
||||
@@ -543,9 +591,11 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
|
||||
const curatedSqlQuery = filters.stashId
|
||||
? 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);
|
||||
}
|
||||
|
||||
@@ -553,32 +603,12 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
|
||||
// console.log(util.inspect(results, null, Infinity));
|
||||
|
||||
const years = results
|
||||
.find((result) => (result.columns[0].years || result.columns[0]['scenes.years']) && result.columns[1]['count(*)'])
|
||||
?.data.map((row) => ({ key: row.years || row['scenes.years'], doc_count: row['count(*)'] }))
|
||||
|| [];
|
||||
|
||||
const actorIds = results
|
||||
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
|
||||
?.data.map((row) => ({ key: row.actor_ids || row['scenes.actor_ids'], doc_count: row['count(*)'] }))
|
||||
|| [];
|
||||
|
||||
const tagIds = results
|
||||
.find((result) => (result.columns[0].tag_ids || result.columns[0]['scenes.tag_ids']) && result.columns[1]['count(*)'])
|
||||
?.data.map((row) => ({ key: row.tag_ids || row['scenes.tag_ids'], doc_count: row['count(*)'] }))
|
||||
|| [];
|
||||
|
||||
const channelIds = results
|
||||
.find((result) => (result.columns[0].channel_id || result.columns[0]['scenes.channel_id']) && result.columns[1]['count(*)'])
|
||||
?.data.map((row) => ({ key: row.channel_id || row['scenes.channel_id'], doc_count: row['count(*)'] }))
|
||||
|| [];
|
||||
|
||||
const studioIds = results
|
||||
.find((result) => (result.columns[0].studio_id || result.columns[0]['scenes.studio_id']) && result.columns[1]['count(*)'])
|
||||
?.data
|
||||
.map((row) => ({ key: row.studio_id || row['scenes.studio_id'], doc_count: row['count(*)'] }))
|
||||
.filter((row) => !!row.key)
|
||||
|| [];
|
||||
const years = curateFacet(results, 'years_facet', 'count(*)');
|
||||
const actorIds = curateFacet(results, 'actors_facet');
|
||||
const tagIds = curateFacet(results, 'tags_facet');
|
||||
const actorTagIds = curateFacet(results, 'actor_tags_facet');
|
||||
const channelIds = curateFacet(results, 'channels_facet');
|
||||
const studioIds = curateFacet(results, 'studios_facet');
|
||||
|
||||
const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found')?.Value) || 0;
|
||||
|
||||
@@ -589,6 +619,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
years,
|
||||
actorIds,
|
||||
tagIds,
|
||||
actorTagIds,
|
||||
channelIds,
|
||||
studioIds,
|
||||
},
|
||||
@@ -603,14 +634,18 @@ function countAggregations(buckets) {
|
||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
}
|
||||
|
||||
export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
export async function fetchScenes(filters, rawOptions, reqUser, context) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
console.log('filters', filters);
|
||||
console.log('options', options);
|
||||
if (argv.debug) {
|
||||
console.log('filters', filters);
|
||||
console.log('options', options);
|
||||
}
|
||||
|
||||
console.time('manticore sql');
|
||||
|
||||
const result = await queryManticoreSql(filters, options, reqUser);
|
||||
|
||||
console.timeEnd('manticore sql');
|
||||
|
||||
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
|
||||
@@ -622,17 +657,18 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
|
||||
console.time('fetch aggregations');
|
||||
|
||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [],
|
||||
const [aggActors, aggTags, aggActorTags, aggChannels] = await Promise.all([
|
||||
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 }, reqUser, context) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.actorTagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||
]);
|
||||
|
||||
console.timeEnd('fetch aggregations');
|
||||
|
||||
console.time('fetch full');
|
||||
const sceneIds = result.scenes.map((scene) => Number(scene.id));
|
||||
const scenes = await fetchScenesById(sceneIds, { reqUser });
|
||||
const scenes = await fetchScenesById(sceneIds, { reqUser, ...context });
|
||||
console.timeEnd('fetch full');
|
||||
|
||||
return {
|
||||
@@ -640,6 +676,7 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
aggYears,
|
||||
aggActors,
|
||||
aggTags,
|
||||
aggActorTags,
|
||||
aggChannels,
|
||||
total: result.total,
|
||||
limit: options.limit,
|
||||
@@ -754,9 +791,10 @@ async function applySceneTagsDelta(sceneId, delta, trx) {
|
||||
|
||||
if (delta.value.length > 0) {
|
||||
await knexOwner('releases_tags')
|
||||
.insert(delta.value.map((tagId) => ({
|
||||
.insert(delta.value.map((tag) => ({
|
||||
release_id: sceneId,
|
||||
tag_id: tagId,
|
||||
tag_id: tag.id,
|
||||
actor_id: tag.actorId,
|
||||
source: 'editor',
|
||||
})))
|
||||
.transacting(trx);
|
||||
|
||||
27
src/tags.js
@@ -1,17 +1,18 @@
|
||||
import knex from './knex.js';
|
||||
import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
import { curateMedia } from './media.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
function curateTag(tag, context) {
|
||||
function curateTag(tag, context = {}) {
|
||||
return {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
slug: tag.slug,
|
||||
description: tag.description,
|
||||
description: context.restriction ? null : tag.description, // censor interferes with markdown
|
||||
priority: tag.priority,
|
||||
poster: tag.poster && curateMedia(tag.poster),
|
||||
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 [tags, posters] = await Promise.all([
|
||||
@@ -55,10 +56,13 @@ export async function fetchTags(options = {}) {
|
||||
}
|
||||
}),
|
||||
knex('tags_posters')
|
||||
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id'),
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
|
||||
]);
|
||||
|
||||
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
|
||||
@@ -66,10 +70,10 @@ export async function fetchTags(options = {}) {
|
||||
return tags.map((tagEntry) => curateTag({
|
||||
...tagEntry,
|
||||
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([
|
||||
knex('tags')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
@@ -80,14 +84,17 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}
|
||||
}),
|
||||
knex('tags_posters')
|
||||
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('tags', 'tags.id', 'tags_posters.tag_id')
|
||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string')),
|
||||
knex('tags_photos')
|
||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string'))
|
||||
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
|
||||
context.restriction ? [] : knex('tags_photos')
|
||||
.select('tags_photos.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('tags', 'tags.id', 'tags_photos.tag_id')
|
||||
.leftJoin('media', 'media.id', 'tags_photos.media_id')
|
||||
@@ -118,6 +125,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}, {
|
||||
alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id),
|
||||
append: options.append,
|
||||
...context,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -136,6 +144,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}, {
|
||||
alerts: alerts.filter((alert) => alert.tag_id === tag.id),
|
||||
append: options.append,
|
||||
...context,
|
||||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { indexApi, utilsApi } from '../manticore.js';
|
||||
import rawvideos from './movies.json' with { type: 'json' };
|
||||
|
||||
async function fetchvideos() {
|
||||
const videos = rawvideos
|
||||
.filter((video) => video.cast.length > 0
|
||||
&& video.genres.length > 0
|
||||
&& video.cast.every((actor) => actor.charCodeAt(0) >= 65)) // throw out videos with non-alphanumerical actor names
|
||||
.map((video, index) => ({ id: index + 1, ...video }));
|
||||
|
||||
const actors = Array.from(new Set(videos.flatMap((video) => video.cast))).sort();
|
||||
const genres = Array.from(new Set(videos.flatMap((video) => video.genres)));
|
||||
|
||||
return {
|
||||
videos,
|
||||
actors,
|
||||
genres,
|
||||
};
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await utilsApi.sql('drop table if exists videos');
|
||||
await utilsApi.sql('drop table if exists videos_liked');
|
||||
|
||||
await utilsApi.sql(`create table videos (
|
||||
id int,
|
||||
title text,
|
||||
actor_ids multi,
|
||||
actors text,
|
||||
genre_ids multi,
|
||||
genres text
|
||||
)`);
|
||||
|
||||
await utilsApi.sql(`create table videos_liked (
|
||||
id int,
|
||||
user_id int,
|
||||
video_id int
|
||||
)`);
|
||||
|
||||
const { videos, actors, genres } = await fetchvideos();
|
||||
|
||||
const likedvideoIds = Array.from(new Set(Array.from({ length: 10.000 }, () => videos[Math.round(Math.random() * videos.length)].id)));
|
||||
|
||||
const docs = videos
|
||||
.map((video) => ({
|
||||
replace: {
|
||||
index: 'videos',
|
||||
id: video.id,
|
||||
doc: {
|
||||
title: video.title,
|
||||
actor_ids: video.cast.map((actor) => actors.indexOf(actor)),
|
||||
actors: video.cast.join(','),
|
||||
genre_ids: video.genres.map((genre) => genres.indexOf(genre)),
|
||||
genres: video.genres.join(','),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.concat(likedvideoIds.map((videoId, index) => ({
|
||||
replace: {
|
||||
index: 'videos_liked',
|
||||
id: index + 1,
|
||||
doc: {
|
||||
user_id: Math.floor(Math.random() * 51),
|
||||
video_id: videoId,
|
||||
},
|
||||
},
|
||||
})));
|
||||
|
||||
const data = await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
console.log('data', data);
|
||||
|
||||
const result = await utilsApi.sql(`
|
||||
select * from videos_liked
|
||||
limit 10
|
||||
`);
|
||||
|
||||
console.log(result[0].data);
|
||||
console.log(result[1]);
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -1,174 +0,0 @@
|
||||
// import config from 'config';
|
||||
import { format } from 'date-fns';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { indexApi } from '../manticore.js';
|
||||
|
||||
import { knexOwner as knex } from '../knex.js';
|
||||
import slugify from '../utils/slugify.js';
|
||||
import chunk from '../utils/chunk.js';
|
||||
|
||||
async function fetchScenes() {
|
||||
const scenes = await knex.raw(`
|
||||
SELECT
|
||||
releases.id AS id,
|
||||
releases.title,
|
||||
releases.created_at,
|
||||
releases.date,
|
||||
releases.shoot_id,
|
||||
scenes_meta.stashed,
|
||||
entities.id as channel_id,
|
||||
entities.slug as channel_slug,
|
||||
entities.name as channel_name,
|
||||
parents.id as network_id,
|
||||
parents.slug as network_slug,
|
||||
parents.name as network_name,
|
||||
COALESCE(JSON_AGG(DISTINCT (actors.id, actors.name)) FILTER (WHERE actors.id IS NOT NULL), '[]') as actors,
|
||||
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name)) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags
|
||||
FROM releases
|
||||
LEFT JOIN scenes_meta ON scenes_meta.scene_id = releases.id
|
||||
LEFT JOIN entities ON releases.entity_id = entities.id
|
||||
LEFT JOIN entities AS parents ON parents.id = entities.parent_id
|
||||
LEFT JOIN releases_actors AS local_actors ON local_actors.release_id = releases.id
|
||||
LEFT JOIN releases_directors AS local_directors ON local_directors.release_id = releases.id
|
||||
LEFT JOIN releases_tags AS local_tags ON local_tags.release_id = releases.id
|
||||
LEFT JOIN actors ON local_actors.actor_id = actors.id
|
||||
LEFT JOIN actors AS directors ON local_directors.director_id = directors.id
|
||||
LEFT JOIN tags ON local_tags.tag_id = tags.id
|
||||
LEFT JOIN tags as tags_aliases ON local_tags.tag_id = tags_aliases.alias_for AND tags_aliases.secondary = true
|
||||
GROUP BY
|
||||
releases.id,
|
||||
releases.title,
|
||||
releases.created_at,
|
||||
releases.date,
|
||||
releases.shoot_id,
|
||||
scenes_meta.stashed,
|
||||
entities.id,
|
||||
entities.name,
|
||||
entities.slug,
|
||||
entities.alias,
|
||||
parents.id,
|
||||
parents.name,
|
||||
parents.slug,
|
||||
parents.alias;
|
||||
`);
|
||||
|
||||
const actors = Object.fromEntries(scenes.rows.flatMap((row) => row.actors.map((actor) => [actor.f1, faker.person.fullName()])));
|
||||
const tags = Object.fromEntries(scenes.rows.flatMap((row) => row.tags.map((tag) => [tag.f1, faker.word.adjective()])));
|
||||
|
||||
return scenes.rows.map((row) => {
|
||||
const title = faker.lorem.lines(1);
|
||||
|
||||
const channelName = faker.company.name();
|
||||
const channelSlug = slugify(channelName, '');
|
||||
|
||||
const networkName = faker.company.name();
|
||||
const networkSlug = slugify(networkName, '');
|
||||
|
||||
const rowActors = row.actors.map((actor) => ({ f1: actor.f1, f2: actors[actor.f1] }));
|
||||
const rowTags = row.tags.map((tag) => ({ f1: tag.f1, f2: tags[tag.f1], f3: tag.f3 }));
|
||||
|
||||
return {
|
||||
...row,
|
||||
title,
|
||||
actors: rowActors,
|
||||
tags: rowTags,
|
||||
channel_name: channelName,
|
||||
channel_slug: channelSlug,
|
||||
network_name: networkName,
|
||||
network_slug: networkSlug,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function updateStashed(docs) {
|
||||
await chunk(docs, 1000).reduce(async (chain, docsChunk) => {
|
||||
await chain;
|
||||
|
||||
const sceneIds = docsChunk.map((doc) => doc.replace.id);
|
||||
|
||||
const stashes = await knex('stashes_scenes')
|
||||
.select('stashes_scenes.id as stashed_id', 'stashes_scenes.scene_id', 'stashes.id as stash_id', 'stashes.user_id as user_id')
|
||||
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
|
||||
.whereIn('scene_id', sceneIds);
|
||||
|
||||
if (stashes.length > 0) {
|
||||
console.log(stashes);
|
||||
}
|
||||
|
||||
const stashDocs = docsChunk.flatMap((doc) => {
|
||||
const sceneStashes = stashes.filter((stash) => stash.scene_id === doc.replace.id);
|
||||
|
||||
if (sceneStashes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stashDoc = sceneStashes.map((stash) => ({
|
||||
replace: {
|
||||
index: 'scenes_stashed',
|
||||
id: stash.stashed_id,
|
||||
doc: {
|
||||
// ...doc.replace.doc,
|
||||
scene_id: doc.replace.id,
|
||||
user_id: stash.user_id,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return stashDoc;
|
||||
});
|
||||
|
||||
console.log(stashDocs);
|
||||
|
||||
if (stashDocs.length > 0) {
|
||||
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const scenes = await fetchScenes();
|
||||
|
||||
const docs = scenes.map((scene) => {
|
||||
const flatActors = scene.actors.flatMap((actor) => actor.f2.match(/[\w']+/g)); // match word characters to filter out brackets etc.
|
||||
const flatTags = scene.tags.filter((tag) => tag.f3 > 6).flatMap((tag) => (tag.f4 ? `${tag.f2} ${tag.f4}` : tag.f2).match(/[\w']+/g)); // only make top tags searchable to minimize cluttered results
|
||||
const filteredTitle = scene.title && [...flatActors, ...flatTags].reduce((accTitle, tag) => accTitle.replace(new RegExp(tag.replace(/[^\w\s]+/g, ''), 'i'), ''), scene.title).trim().replace(/\s{2,}/, ' ');
|
||||
|
||||
return {
|
||||
replace: {
|
||||
index: 'scenes',
|
||||
id: scene.id,
|
||||
doc: {
|
||||
title: scene.title || undefined,
|
||||
title_filtered: filteredTitle || undefined,
|
||||
date: scene.date ? Math.round(scene.date.getTime() / 1000) : undefined,
|
||||
created_at: Math.round(scene.created_at.getTime() / 1000),
|
||||
effective_date: Math.round((scene.date || scene.created_at).getTime() / 1000),
|
||||
// shoot_id: scene.shoot_id || undefined,
|
||||
channel_id: scene.channel_id,
|
||||
channel_slug: scene.channel_slug,
|
||||
channel_name: scene.channel_name,
|
||||
network_id: scene.network_id || undefined,
|
||||
network_slug: scene.network_slug || undefined,
|
||||
network_name: scene.network_name || undefined,
|
||||
actor_ids: scene.actors.map((actor) => actor.f1),
|
||||
actors: scene.actors.map((actor) => actor.f2).join(),
|
||||
tag_ids: scene.tags.map((tag) => tag.f1),
|
||||
tags: flatTags.join(' '),
|
||||
meta: scene.date ? format(scene.date, 'y yy M MMM MMMM d') : undefined,
|
||||
liked: scene.stashed || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const data = await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
await updateStashed(docs);
|
||||
|
||||
console.log('data', data);
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -1,50 +0,0 @@
|
||||
import { indexApi, utilsApi } from '../manticore.js';
|
||||
import { knexOwner as knex } from '../knex.js';
|
||||
import chunk from '../utils/chunk.js';
|
||||
|
||||
async function syncStashes(domain = 'scene') {
|
||||
await utilsApi.sql(`truncate table ${domain}s_stashed`);
|
||||
|
||||
const stashes = await knex(`stashes_${domain}s`)
|
||||
.select(
|
||||
`stashes_${domain}s.id as stashed_id`,
|
||||
`stashes_${domain}s.${domain}_id`,
|
||||
'stashes.id as stash_id',
|
||||
'stashes.user_id as user_id',
|
||||
`stashes_${domain}s.created_at as created_at`,
|
||||
)
|
||||
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`);
|
||||
|
||||
await chunk(stashes, 1000).reduce(async (chain, stashChunk, index) => {
|
||||
await chain;
|
||||
|
||||
const stashDocs = stashChunk.map((stash) => ({
|
||||
replace: {
|
||||
index: `${domain}s_stashed`,
|
||||
id: stash.stashed_id,
|
||||
doc: {
|
||||
[`${domain}_id`]: stash[`${domain}_id`],
|
||||
stash_id: stash.stash_id,
|
||||
user_id: stash.user_id,
|
||||
created_at: Math.round(stash.created_at.getTime() / 1000),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
console.log(`Synced ${index * 1000 + stashChunk.length}/${stashes.length} ${domain} stashes`);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await syncStashes('scene');
|
||||
await syncStashes('actor');
|
||||
await syncStashes('movie');
|
||||
|
||||
console.log('Done!');
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import argv from '../argv.js';
|
||||
|
||||
import {
|
||||
login,
|
||||
@@ -14,6 +15,10 @@ import {
|
||||
import { fetchUser } from '../users.js';
|
||||
|
||||
function getIp(req) {
|
||||
if (argv.ip) {
|
||||
return argv.ip;
|
||||
}
|
||||
|
||||
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
|
||||
|
||||
const unmappedIp = ip?.includes('.')
|
||||
|
||||
@@ -51,6 +51,7 @@ export default async function mainHandler(req, res, next) {
|
||||
siteKey: config.auth.captcha.siteKey,
|
||||
},
|
||||
},
|
||||
restriction: req.restriction,
|
||||
meta: {
|
||||
now: new Date().toISOString(),
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function fetchMoviesApi(req, res) {
|
||||
} = await fetchMovies(await curateMoviesQuery(req.query), {
|
||||
page: Number(req.query.page) || 1,
|
||||
limit: Number(req.query.limit) || 30,
|
||||
}, req.user);
|
||||
}, req.user, { restriction: req.restriction });
|
||||
|
||||
res.send(stringify({
|
||||
movies,
|
||||
@@ -47,7 +47,7 @@ export async function fetchMoviesApi(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) {
|
||||
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,
|
||||
limit: query.limit || 30,
|
||||
aggregate: false,
|
||||
}, req.user);
|
||||
}, req.user, { restriction: req.restriction });
|
||||
|
||||
return {
|
||||
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;
|
||||
}
|
||||
@@ -59,6 +59,7 @@ async function fetchScenesApi(req, res) {
|
||||
aggYears,
|
||||
aggActors,
|
||||
aggTags,
|
||||
aggActorTags,
|
||||
aggChannels,
|
||||
limit,
|
||||
total,
|
||||
@@ -68,13 +69,16 @@ async function fetchScenesApi(req, res) {
|
||||
}), {
|
||||
page: Number(req.query.page) || 1,
|
||||
limit: Number(req.query.limit) || 30,
|
||||
}, req.user);
|
||||
}, req.user, {
|
||||
restriction: req.restriction,
|
||||
});
|
||||
|
||||
res.send(stringify({
|
||||
scenes,
|
||||
aggYears,
|
||||
aggActors,
|
||||
aggTags,
|
||||
aggActorTags,
|
||||
aggChannels,
|
||||
limit,
|
||||
total,
|
||||
@@ -250,7 +254,7 @@ export async function fetchScenesGraphql(query, req) {
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
|
||||
@@ -263,6 +267,7 @@ export async function fetchScenesByIdGraphql(query, req) {
|
||||
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
||||
reqUser: req.user,
|
||||
includePartOf: true,
|
||||
restriction: req.restriction,
|
||||
});
|
||||
|
||||
if (query.ids) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import redis from '../redis.js';
|
||||
|
||||
import errorHandler from './error.js';
|
||||
import consentHandler from './consent.js';
|
||||
import initRestrictionHandler from './restrictions.js';
|
||||
|
||||
import { scenesRouter } from './scenes.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 alertsRouter } from './alerts.js';
|
||||
|
||||
import { initCachesApi } from './system.js';
|
||||
|
||||
import initLogger from '../logger.js';
|
||||
|
||||
const logger = initLogger();
|
||||
@@ -48,9 +51,11 @@ const isProduction = process.env.NODE_ENV === 'production';
|
||||
export default async function initServer() {
|
||||
const app = express();
|
||||
const router = Router();
|
||||
const restrictionHandler = await initRestrictionHandler();
|
||||
|
||||
app.use(compression());
|
||||
app.disable('x-powered-by');
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
router.use(boolParser());
|
||||
|
||||
@@ -58,7 +63,7 @@ export default async function initServer() {
|
||||
router.use('/', express.static('static'));
|
||||
router.use('/media', express.static(config.media.path));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
router.use((req, _res, next) => {
|
||||
if (req.headers.cookie) {
|
||||
const cookies = cookie.parse(req.headers.cookie);
|
||||
|
||||
@@ -109,11 +114,13 @@ export default async function initServer() {
|
||||
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'));
|
||||
});
|
||||
|
||||
router.use('/api/*', async (req, res, next) => {
|
||||
router.use('/api/*', async (req, _res, next) => {
|
||||
if (req.headers['api-user']) {
|
||||
await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
||||
|
||||
@@ -153,13 +160,15 @@ export default async function initServer() {
|
||||
// TAGS
|
||||
router.get('/api/tags', fetchTagsApi);
|
||||
|
||||
router.post('/api/caches', initCachesApi);
|
||||
|
||||
if (config.apiAccess.graphqlEnabled) {
|
||||
router.post('/graphql', graphqlApi);
|
||||
}
|
||||
|
||||
router.use(consentHandler);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
router.use((_req, res, next) => {
|
||||
/* eslint-disable no-param-reassign */
|
||||
res.set('Accept-CH', 'Sec-CH-Prefers-Color-Scheme');
|
||||
res.set('Vary', 'Sec-CH-Prefers-Color-Scheme');
|
||||
|
||||
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) {
|
||||
const tags = await fetchTags({
|
||||
query: req.query.query,
|
||||
}, {
|
||||
restriction: req.restriction,
|
||||
});
|
||||
|
||||
res.send(tags);
|
||||
|
||||