Compare commits
69 Commits
62753a4af0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 343325440e | |||
| 5c018892d3 | |||
| be61293cbe | |||
| e493194ce1 | |||
| b61631c33c | |||
| fa65da75bc | |||
| a4468f18dc | |||
| fea28b71ba | |||
| 884ad891f3 | |||
| 058161f798 | |||
| aa68748817 | |||
| 928857596f | |||
| e6919a4283 | |||
| f7993a9108 | |||
| 134664095a | |||
| 7b2495cef5 | |||
| 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 |
@@ -119,12 +119,22 @@
|
||||
|
||||
.button-cancel {
|
||||
background: none;
|
||||
color: var(--glass);
|
||||
color: var(--error);
|
||||
font-weight: normal;
|
||||
box-shadow: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--error);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--error);
|
||||
color: var(--text-light);
|
||||
background: var(--error);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -66,6 +66,7 @@ const props = defineProps({
|
||||
|
||||
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'}`;
|
||||
}
|
||||
@@ -100,18 +101,8 @@ const bannerSrc = (() => {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding: 4px; /* margin for drop shadow */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(125%);
|
||||
}
|
||||
|
||||
.dark .campaign-overlay {
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(75%);
|
||||
}
|
||||
|
||||
.campaign.backdrop .campaign-overlay {
|
||||
box-shadow: inset 0 0 5px var(--shadow-weak-20);
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
@@ -119,7 +110,21 @@ const bannerSrc = (() => {
|
||||
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 {
|
||||
|
||||
@@ -266,7 +266,7 @@ const expanded = ref(new Set());
|
||||
|
||||
const mappedKeys = {
|
||||
actors: actorsById,
|
||||
tags: tagsById,
|
||||
// tags: tagsById,
|
||||
movies: moviesById,
|
||||
};
|
||||
|
||||
@@ -292,6 +292,16 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
}))];
|
||||
}
|
||||
|
||||
if (key === 'tags') {
|
||||
return [key, value.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.actorId
|
||||
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
|
||||
: tagsById.value[tag.id]?.name,
|
||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaTag) => deltaTag.id === tag.id && (!Object.hasOwn(tag, 'actorId') || deltaTag.actorId === tag.actorId))),
|
||||
}))];
|
||||
}
|
||||
|
||||
if (key === 'socials') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return [key, value.map((item) => ({
|
||||
@@ -323,6 +333,19 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'tags') {
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.actorId
|
||||
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
|
||||
: tagsById.value[tag.id]?.name,
|
||||
modified: !revision.base[delta.key].some((baseTag) => baseTag.id === tag.id && (!Object.hasOwn(baseTag, 'actorId') || baseTag.actorId === tag.actorId)),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'socials') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return {
|
||||
@@ -378,6 +401,8 @@ async function reviewRevision(revision, isApproved) {
|
||||
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
||||
isApproved,
|
||||
feedback: feedbacks.value[revision.id],
|
||||
}, {
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
<template>
|
||||
<div class="tags-sections">
|
||||
<div
|
||||
v-for="actorTags in tags"
|
||||
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||
class="tags-section"
|
||||
>
|
||||
<ul
|
||||
class="tags nolist"
|
||||
:class="{ disabled: !editing.has(item.key) }"
|
||||
>
|
||||
<li
|
||||
v-for="tag in [...item.value, ...newTags]"
|
||||
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((tagId) => tagId === tag.id) }"
|
||||
: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((tagId) => tagId === tag.id)"
|
||||
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.id))"
|
||||
@click="emit('tags', edits.tags.concat(tag))"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
icon="cross2"
|
||||
class="remove"
|
||||
@click="emit('tags', edits.tags.filter((tagId) => tagId !== tag.id))"
|
||||
@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="addTag"
|
||||
@tag="(tag) => addTag(tag, actorTags.actor)"
|
||||
>
|
||||
<Icon
|
||||
icon="plus3"
|
||||
@@ -38,6 +49,8 @@
|
||||
</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>
|
||||
|
||||
<template v-if="isActorTagsAvailable">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-show="order === 'priority'"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'count'"
|
||||
>
|
||||
<Icon
|
||||
icon="star"
|
||||
/>
|
||||
<Icon icon="star" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -115,41 +133,60 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
actorTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
|
||||
const {
|
||||
tag: pageTag,
|
||||
actor: pageActor,
|
||||
stash: pageStash,
|
||||
} = 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(!!pageActor);
|
||||
|
||||
const priorityTags = [
|
||||
'anal',
|
||||
'dp',
|
||||
'threesome',
|
||||
'gangbang',
|
||||
'blowbang',
|
||||
'transsexual',
|
||||
'orgy',
|
||||
'airtight',
|
||||
'dp',
|
||||
'dap',
|
||||
'dvp',
|
||||
'triple-penetration',
|
||||
'tap',
|
||||
'tvp',
|
||||
'transsexual',
|
||||
'spitroast',
|
||||
'mfm',
|
||||
'fmf',
|
||||
'threesome',
|
||||
'bdsm',
|
||||
'deepthroat',
|
||||
'blowjob',
|
||||
'lesbian',
|
||||
];
|
||||
|
||||
const isActorTagsAvailable = computed(() => props.actorTags && (props.filters.actors.length > 0 || pageActor) && !pageStash);
|
||||
|
||||
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)
|
||||
// can't show actor tags inside stash, because both require a join, and manticore currently only supports one
|
||||
const tags = showActorTags.value && isActorTagsAvailable.value
|
||||
? props.actorTags
|
||||
: 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}`"
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<TagsFilter
|
||||
:filters="filters"
|
||||
:tags="aggTags"
|
||||
:actor-tags="aggActorTags"
|
||||
@update="updateFilter"
|
||||
/>
|
||||
|
||||
@@ -263,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));
|
||||
@@ -363,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;
|
||||
|
||||
@@ -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}`"
|
||||
@@ -134,7 +135,10 @@ 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',
|
||||
|
||||
@@ -174,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],
|
||||
|
||||
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "traxxx-web",
|
||||
"version": "0.46.11",
|
||||
"version": "0.49.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.46.11",
|
||||
"version": "0.49.2",
|
||||
"dependencies": {
|
||||
"@brillout/json-serializer": "^0.5.8",
|
||||
"@dicebear/collection": "^7.0.5",
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"overrides": {
|
||||
"vite": "$vite"
|
||||
},
|
||||
"version": "0.46.11",
|
||||
"version": "0.49.2",
|
||||
"imports": {
|
||||
"#/*": "./*.js"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -140,12 +140,20 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
v-if="scene.tags.length > 0"
|
||||
class="tags nolist"
|
||||
<div class="tags">
|
||||
<div
|
||||
v-for="actorTags in tags"
|
||||
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||
class="tags-section"
|
||||
>
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-for="tag in scene.tags"
|
||||
v-if="actorTags.actor"
|
||||
class="tags-actor"
|
||||
>{{ actorTags.actor.name }}:</li>
|
||||
|
||||
<li
|
||||
v-for="tag in actorTags.tags"
|
||||
:key="`tag-${tag.id}`"
|
||||
>
|
||||
<Link
|
||||
@@ -154,6 +162,8 @@
|
||||
>{{ tag.name }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="scene.movies.length > 0 || scene.series.length > 0"
|
||||
@@ -440,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 = {
|
||||
@@ -640,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;
|
||||
|
||||
@@ -28,6 +28,10 @@ export async function onBeforeRender(pageContext) {
|
||||
restriction: pageContext.restriction,
|
||||
});
|
||||
|
||||
if (!scene) {
|
||||
throw render(404, `Cannot find scene '${pageContext.routeParams.sceneId}'.`);
|
||||
}
|
||||
|
||||
const [campaigns, tagIds] = await Promise.all([
|
||||
getRandomCampaigns([
|
||||
{
|
||||
@@ -44,10 +48,6 @@ export async function onBeforeRender(pageContext) {
|
||||
], 'tags', true),
|
||||
]);
|
||||
|
||||
if (!scene) {
|
||||
throw render(404, `Cannot find scene '${pageContext.routeParams.sceneId}'.`);
|
||||
}
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: getTitle(scene),
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
v-if="item.note"
|
||||
v-tooltip="item.note"
|
||||
icon="info2"
|
||||
class="item-note"
|
||||
class="item-note noselect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<Icon
|
||||
v-if="!item.forced"
|
||||
icon="pencil5"
|
||||
class="noselect"
|
||||
:class="{ active: editing.has(item.key) }"
|
||||
@click="toggleField(item)"
|
||||
/>
|
||||
@@ -134,6 +135,15 @@
|
||||
:disabled="!editing.has(item.key)"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="item.type === 'checkbox'"
|
||||
:label="item.checkboxLabel"
|
||||
:checked="edits[item.key]"
|
||||
:disabled="!editing.has(item.key)"
|
||||
class="checkbox delete"
|
||||
@change="(checked) => setDelete(checked)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="item.type === 'date'"
|
||||
class="date"
|
||||
@@ -210,9 +220,10 @@
|
||||
<div class="editor-actions">
|
||||
<Checkbox
|
||||
v-if="user.role !== 'user'"
|
||||
v-tooltip="isApplyDisabled && editing.has('delete') ? 'Delete must be approved by an admin' : null"
|
||||
label="Approve and apply immediately"
|
||||
:checked="apply"
|
||||
:disabled="editing.size === 0"
|
||||
:disabled="isApplyDisabled"
|
||||
@change="(checked) => apply = checked"
|
||||
/>
|
||||
|
||||
@@ -241,10 +252,7 @@ import EditTags from '#/components/edit/tags.vue';
|
||||
import EditMovies from '#/components/edit/movies.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import {
|
||||
// get,
|
||||
post,
|
||||
} from '#/src/api.js';
|
||||
import { post } from '#/src/api.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
@@ -261,6 +269,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',
|
||||
@@ -308,12 +318,20 @@ const fields = computed(() => [
|
||||
},
|
||||
...(user.role === 'user'
|
||||
? []
|
||||
: [{
|
||||
: [
|
||||
{
|
||||
key: 'comment',
|
||||
type: 'text',
|
||||
placeholder: 'Do NOT use this field to summarize and clarify your revision. This field is for permanent notes and comments regarding the scene or database entry itself.',
|
||||
value: scene.value.comment,
|
||||
}]),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
type: 'checkbox',
|
||||
checkboxLabel: 'Remove this scene from the database',
|
||||
value: false,
|
||||
},
|
||||
]),
|
||||
]);
|
||||
|
||||
function simplifyArray(field) {
|
||||
@@ -330,6 +348,9 @@ const comment = ref(null);
|
||||
const apply = ref(user.role !== 'user');
|
||||
const submitted = ref(false);
|
||||
|
||||
const userCanDelete = user.abilities.some((ability) => ability.subject === 'scene' && ability.action === 'delete');
|
||||
const isApplyDisabled = computed(() => editing.value.size === 0 || (edits.value.delete && !userCanDelete));
|
||||
|
||||
const keyMap = {
|
||||
date: {
|
||||
date: 'date',
|
||||
@@ -357,6 +378,14 @@ function setDuration(unit, event) {
|
||||
edits.value.duration[timeUnits.indexOf(unit)] = Number(event.target.value);
|
||||
}
|
||||
|
||||
function setDelete(checked) {
|
||||
edits.value.delete = checked;
|
||||
|
||||
if (!userCanDelete) {
|
||||
apply.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
await post('/revisions/scenes', {
|
||||
@@ -415,6 +444,10 @@ async function submit() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .25rem 1rem;
|
||||
|
||||
.value.disabled {
|
||||
color: var(--glass);
|
||||
}
|
||||
}
|
||||
|
||||
.key {
|
||||
@@ -486,7 +519,7 @@ async function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
.item-note{
|
||||
.item-note {
|
||||
fill: var(--glass);
|
||||
padding: .5rem .75rem;
|
||||
cursor: help;
|
||||
@@ -516,6 +549,25 @@ async function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox.delete {
|
||||
display: inline-flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value.disabled .delete {
|
||||
:deep(.check-checkbox) + .check {
|
||||
background: var(--glass-weak-30);
|
||||
}
|
||||
}
|
||||
|
||||
.value:not(.disabled) .delete {
|
||||
:deep(.check-checkbox:checked) + .check {
|
||||
background: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -538,6 +590,27 @@ async function submit() {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.delete-title {
|
||||
display: block;
|
||||
margin-top: .5rem;
|
||||
max-width: 25rem;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dialog-section {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media(--small) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -208,8 +208,16 @@ 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.*')
|
||||
.whereIn('actors.id', actorIds)
|
||||
.modify((builder) => {
|
||||
if (options.order) {
|
||||
builder.orderBy(...options.order);
|
||||
}
|
||||
|
||||
if (!options.shallow) {
|
||||
builder
|
||||
.select(
|
||||
'actors.*',
|
||||
'actors_meta.stashed',
|
||||
knex.raw('row_to_json(avatars) as avatar'),
|
||||
'birth_countries.alpha2 as birth_country_alpha2',
|
||||
@@ -225,13 +233,9 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'actors.entity_id')
|
||||
.whereIn('actors.id', actorIds)
|
||||
.modify((builder) => {
|
||||
if (options.order) {
|
||||
builder.orderBy(...options.order);
|
||||
.groupBy('actors.id', 'avatars.id', 'sfw_media.id', 'entities.id', 'actors_meta.stashed', 'birth_countries.alpha2', 'residence_countries.alpha2');
|
||||
}
|
||||
})
|
||||
.groupBy('actors.id', 'avatars.id', 'sfw_media.id', 'entities.id', 'actors_meta.stashed', 'birth_countries.alpha2', 'residence_countries.alpha2'),
|
||||
}),
|
||||
knex('actors_profiles')
|
||||
.select(
|
||||
'actors_profiles.*',
|
||||
@@ -271,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,
|
||||
}));
|
||||
}
|
||||
@@ -290,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);
|
||||
|
||||
@@ -6,7 +6,7 @@ function getWatchUrl(scene) {
|
||||
return new URL(scene.url).href;
|
||||
}
|
||||
|
||||
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network')) {
|
||||
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network' || !scene.network)) {
|
||||
return new URL(scene.channel.url).href;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ export async function fetchMovies(filters, rawOptions, reqUser, context) {
|
||||
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 }, reqUser) : [],
|
||||
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) : [],
|
||||
]);
|
||||
|
||||
158
src/scenes.js
@@ -45,6 +45,7 @@ function curateScene(rawScene, assets, reqUser, context) {
|
||||
slug: assets.channel.slug,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
url: assets.channel.url,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
},
|
||||
@@ -52,6 +53,7 @@ function curateScene(rawScene, assets, reqUser, context) {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
url: assets.network_url,
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.network_has_logo,
|
||||
} : null,
|
||||
@@ -82,6 +84,7 @@ function curateScene(rawScene, assets, reqUser, context) {
|
||||
slug: tag.slug,
|
||||
name: censor(tag.name, context.restriction),
|
||||
priority: tag.priority,
|
||||
actorId: tag.actor_id,
|
||||
})),
|
||||
chapters: assets.chapters.map((chapter) => ({
|
||||
id: chapter.id,
|
||||
@@ -137,7 +140,11 @@ function curateScene(rawScene, assets, reqUser, context) {
|
||||
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
|
||||
|
||||
if (!isVideoRestricted || reqUser?.abilities?.some((ability) => ability.trailerAccess)) {
|
||||
if (reqUser) {
|
||||
// only show trailers to logged in users to curb S3 traffic
|
||||
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted });
|
||||
}
|
||||
|
||||
curatedScene.teaser = curateMedia(assets.teaser, { type: 'teaser', isRestricted: isVideoRestricted });
|
||||
}
|
||||
|
||||
@@ -174,6 +181,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
'networks.slug as network_slug',
|
||||
'networks.name as network_name',
|
||||
'networks.type as network_type',
|
||||
'networks.url as network_url',
|
||||
'networks.has_logo as network_has_logo',
|
||||
knex.raw('row_to_json(affiliates) as affiliate'),
|
||||
)
|
||||
@@ -214,7 +222,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.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)
|
||||
@@ -397,6 +405,15 @@ function curateOptions(options) {
|
||||
};
|
||||
}
|
||||
|
||||
// function curateFacet(results, field, count = 'count(distinct id)') {
|
||||
function curateFacet(results, field) {
|
||||
return results
|
||||
.find((result) => result.columns[0][field] && (result.columns[1]['count(distinct id)'] || result.columns[1]['count(*)']))
|
||||
?.data.map((row) => ({ key: row[field], doc_count: row['count(distinct id)'] || row['count(*)'] }))
|
||||
.filter((row) => !!row.key)
|
||||
|| [];
|
||||
}
|
||||
|
||||
async function queryManticoreSql(filters, options, _reqUser) {
|
||||
const aggSize = config.database.manticore.maxAggregateSize;
|
||||
|
||||
@@ -419,6 +436,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
:yearsFacet:
|
||||
:actorsFacet:
|
||||
:tagsFacet:
|
||||
:actorTagsFacet:
|
||||
:channelsFacet:
|
||||
:studiosFacet:;
|
||||
show meta;
|
||||
@@ -451,9 +469,15 @@ 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
|
||||
builder
|
||||
.leftJoin('scenes_tags', 'scenes_tags.scene_id', 'scenes_.id')
|
||||
.groupBy('scenes.id');
|
||||
}
|
||||
|
||||
if (filters.query) {
|
||||
// we exclude title because we have a curated title_filtered field for more effective results
|
||||
builder.whereRaw('match(\'@!title :query:\', scenes)', { query: escape(filters.query) });
|
||||
}
|
||||
|
||||
@@ -553,11 +577,15 @@ 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(*) 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 distinct id order by count(*) 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(*) desc limit ?`, [aggSize])
|
||||
: null,
|
||||
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id as channels_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
||||
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id as studios_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
||||
maxMatches: config.database.manticore.maxMatches,
|
||||
maxQueryTime: config.database.manticore.maxQueryTime,
|
||||
}).toString();
|
||||
@@ -565,7 +593,9 @@ 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' && argv.debug) {
|
||||
console.log(curatedSqlQuery);
|
||||
@@ -573,34 +603,12 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
|
||||
const results = await utilsApi.sql(curatedSqlQuery);
|
||||
|
||||
// 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');
|
||||
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;
|
||||
|
||||
@@ -611,6 +619,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
years,
|
||||
actorIds,
|
||||
tagIds,
|
||||
actorTagIds,
|
||||
channelIds,
|
||||
studioIds,
|
||||
},
|
||||
@@ -648,9 +657,10 @@ export async function fetchScenes(filters, rawOptions, reqUser, context) {
|
||||
|
||||
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 }, reqUser) : [],
|
||||
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) : [],
|
||||
]);
|
||||
|
||||
@@ -666,6 +676,7 @@ export async function fetchScenes(filters, rawOptions, reqUser, context) {
|
||||
aggYears,
|
||||
aggActors,
|
||||
aggTags,
|
||||
aggActorTags,
|
||||
aggChannels,
|
||||
total: result.total,
|
||||
limit: options.limit,
|
||||
@@ -718,8 +729,14 @@ export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
|
||||
const actorIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.actors, ...(revision.deltas.find((delta) => delta.key === 'actors')?.value || [])])));
|
||||
const tagIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.tags, ...(revision.deltas.find((delta) => delta.key === 'tags')?.value || [])])));
|
||||
const actorIds = Array.from(new Set(revisions.flatMap((revision) => [
|
||||
...revision.base.actors,
|
||||
...(revision.deltas.find((delta) => delta.key === 'actors')?.value || []),
|
||||
...revision.base.tags.map((tag) => tag.actorId),
|
||||
...revision.deltas.find((delta) => delta.key === 'tags')?.value.map((tag) => tag.actorId) || [],
|
||||
].filter(Boolean))));
|
||||
|
||||
const tagIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.tags, ...(revision.deltas.find((delta) => delta.key === 'tags')?.value || [])].map((tag) => tag.id))));
|
||||
const movieIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.movies, ...(revision.deltas.find((delta) => delta.key === 'movies')?.value || [])])));
|
||||
|
||||
const [actors, tags, movies] = await Promise.all([
|
||||
@@ -780,9 +797,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);
|
||||
@@ -805,7 +823,18 @@ async function applySceneMoviesDelta(sceneId, delta, trx) {
|
||||
}
|
||||
}
|
||||
|
||||
async function applySceneRevision(revisionIds) {
|
||||
async function applySceneDeleteDelta(sceneId, _delta, trx, reqUser) {
|
||||
if (!reqUser.abilities.some((ability) => ability.subject === 'scene' && ability.action === 'delete')) {
|
||||
throw new HttpError('You are not privileged to delete scenes', 400);
|
||||
}
|
||||
|
||||
await knexOwner('releases')
|
||||
.where('id', sceneId)
|
||||
.delete()
|
||||
.transacting(trx);
|
||||
}
|
||||
|
||||
async function applySceneRevision(revisionIds, reqUser) {
|
||||
const revisions = await knexOwner('scenes_revisions')
|
||||
.whereIn('id', revisionIds)
|
||||
.whereNull('applied_at'); // should not re-apply revision that was already applied
|
||||
@@ -815,6 +844,10 @@ async function applySceneRevision(revisionIds) {
|
||||
|
||||
await knexOwner.transaction(async (trx) => {
|
||||
await Promise.all(revision.deltas.map(async (delta) => {
|
||||
if (delta.key === 'delete') {
|
||||
return applySceneDeleteDelta(revision.scene_id, delta, trx, reqUser);
|
||||
}
|
||||
|
||||
if ([
|
||||
'title',
|
||||
'description',
|
||||
@@ -846,11 +879,13 @@ async function applySceneRevision(revisionIds) {
|
||||
|
||||
await knexOwner('scenes_revisions')
|
||||
.where('id', revision.id)
|
||||
.update('applied_at', knex.fn.now());
|
||||
.update('applied_at', knexOwner.fn.now())
|
||||
.transacting(trx);
|
||||
|
||||
// await trx.commit();
|
||||
}).catch(async (error) => {
|
||||
logger.error(`Failed to apply revision ${revision.id} on scene ${revision.scene_id}: ${error.message}`);
|
||||
throw error;
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
@@ -880,7 +915,19 @@ export async function reviewSceneRevision(revisionId, isApproved, { feedback },
|
||||
}
|
||||
|
||||
if (isApproved) {
|
||||
await applySceneRevision([revisionId]);
|
||||
try {
|
||||
await applySceneRevision([revisionId], reqUser);
|
||||
} catch (error) {
|
||||
await knexOwner('scenes_revisions')
|
||||
.where('id', revisionId)
|
||||
.update({
|
||||
approved: null,
|
||||
reviewed_at: null,
|
||||
reviewed_by: null,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,6 +973,13 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
|
||||
return [key, values.id];
|
||||
}
|
||||
|
||||
if (key === 'tags') {
|
||||
return [key, values.map((tag) => ({
|
||||
id: tag.id,
|
||||
actorId: tag.actorId,
|
||||
}))];
|
||||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||
}
|
||||
@@ -934,10 +988,24 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
|
||||
}).filter(Boolean));
|
||||
|
||||
const deltas = Object.entries(edits).map(([key, value]) => {
|
||||
if (key === 'delete') {
|
||||
return { key: 'delete' };
|
||||
}
|
||||
|
||||
if (baseScene[key] === value || typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'tags') {
|
||||
return {
|
||||
key,
|
||||
value: value.map((tag) => ({
|
||||
id: tag.id,
|
||||
actorId: tag.actorId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const valueSet = new Set(value);
|
||||
const baseSet = new Set(baseScene[key]);
|
||||
@@ -972,6 +1040,6 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
|
||||
|
||||
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
||||
// don't keep the editor waiting for the revision to apply
|
||||
reviewSceneRevision(revisionEntry.id, true, {}, reqUser);
|
||||
reviewSceneRevision(revisionEntry.id, true, {}, reqUser).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
648028
src/tools/movies.json
80
src/tools/scene_tag_revision_fix.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { MerkleJson } from 'merkle-json';
|
||||
|
||||
import knex from '../knex.js';
|
||||
|
||||
const mj = new MerkleJson();
|
||||
|
||||
function curateTag(tag) {
|
||||
if (Object.hasOwn(tag, 'actorId')) {
|
||||
return {
|
||||
id: tag.id,
|
||||
actorId: tag.actorId,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof tag === 'number') {
|
||||
return {
|
||||
id: tag,
|
||||
// can't restore actorId, don't set to null to hint at missing data
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unrecognized tag delta: ${JSON.stringify(tag)}`);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const revisions = await knex('scenes_revisions');
|
||||
|
||||
// console.log(revisions);
|
||||
|
||||
const fixedRevisions = revisions.map((revision) => {
|
||||
if (revision.base.tags.length === 0 && !revision.deltas.some((delta) => delta.key === 'tags')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newDeltas = revision.deltas.map((delta) => {
|
||||
if (delta.key !== 'tags') {
|
||||
return delta;
|
||||
}
|
||||
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((tag) => curateTag(tag)),
|
||||
};
|
||||
});
|
||||
|
||||
const newBase = {
|
||||
...revision.base,
|
||||
tags: revision.base.tags.map((tag) => curateTag(tag)),
|
||||
};
|
||||
|
||||
return {
|
||||
...revision,
|
||||
deltas: newDeltas,
|
||||
base: newBase,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
const entries = fixedRevisions.map((revision) => ({
|
||||
id: revision.id,
|
||||
base: JSON.stringify(revision.base),
|
||||
deltas: JSON.stringify(revision.deltas),
|
||||
hash: mj.hash({
|
||||
base: revision.base,
|
||||
deltas: revision.deltas,
|
||||
}),
|
||||
}));
|
||||
|
||||
console.log(entries);
|
||||
|
||||
await knex('scenes_revisions')
|
||||
.insert(entries)
|
||||
.onConflict('id')
|
||||
.merge(['base', 'deltas', 'hash']);
|
||||
|
||||
console.log(`Fixed ${entries.length} revisions`);
|
||||
|
||||
await 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();
|
||||
@@ -11,7 +11,7 @@ export default async function initRestrictionHandler() {
|
||||
const reader = await Reader.open('assets/GeoLite2-City.mmdb');
|
||||
|
||||
function getRestriction(req) {
|
||||
if (req.session.restriction && req.session.country && req.session.restrictionIp === req.userIp) {
|
||||
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,
|
||||
@@ -71,6 +71,10 @@ export default async function initRestrictionHandler() {
|
||||
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();
|
||||
|
||||
@@ -59,6 +59,7 @@ async function fetchScenesApi(req, res) {
|
||||
aggYears,
|
||||
aggActors,
|
||||
aggTags,
|
||||
aggActorTags,
|
||||
aggChannels,
|
||||
limit,
|
||||
total,
|
||||
@@ -77,6 +78,7 @@ async function fetchScenesApi(req, res) {
|
||||
aggYears,
|
||||
aggActors,
|
||||
aggTags,
|
||||
aggActorTags,
|
||||
aggChannels,
|
||||
limit,
|
||||
total,
|
||||
|
||||
@@ -41,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();
|
||||
@@ -158,6 +160,8 @@ export default async function initServer() {
|
||||
// TAGS
|
||||
router.get('/api/tags', fetchTagsApi);
|
||||
|
||||
router.post('/api/caches', initCachesApi);
|
||||
|
||||
if (config.apiAccess.graphqlEnabled) {
|
||||
router.post('/graphql', graphqlApi);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||