cyberes revised this gist . Go to revision
1 file changed, 5 insertions, 3 deletions
server.py
@@ -77,14 +77,14 @@ def get_nodes(): | |||
77 | 77 | ||
78 | 78 | ||
79 | 79 | @cache.memoize(timeout=900) | |
80 | - | def get_items_by_topics(topics: list): | |
80 | + | def get_items_by_topics(included_topics, excluded_topics): | |
81 | 81 | count_raw = redis_conn.get('node_count') | |
82 | 82 | if not count_raw or not int(count_raw): | |
83 | 83 | return [] | |
84 | 84 | nodes = get_nodes()['data']['nodes'] | |
85 | 85 | result = [] | |
86 | 86 | for node in nodes: | |
87 | - | if set(topics).issubset(set(node['topics'])): | |
87 | + | if set(included_topics).issubset(set(node['topics'])) and not set(excluded_topics).intersection(set(node['topics'])): | |
88 | 88 | result.append(node) | |
89 | 89 | return result | |
90 | 90 | ||
@@ -125,7 +125,9 @@ def random_character(): | |||
125 | 125 | response_data['timestamp'] = c['timestamp'] | |
126 | 126 | else: | |
127 | 127 | tags = tags_arg.split(',') | |
128 | - | matches = get_items_by_topics(tags) | |
128 | + | included_tags = [x for x in tags if not x.startswith('-')] | |
129 | + | excluded_tags = [x.lstrip('-') for x in tags if x.startswith('-')] | |
130 | + | matches = get_items_by_topics(included_tags, excluded_tags) | |
129 | 131 | if len(matches): | |
130 | 132 | response_data = random.choice(matches) | |
131 | 133 | response_data['timestamp'] = c['timestamp'] |
cyberes revised this gist . Go to revision
1 file changed, 141 insertions
server.py(file created)
@@ -0,0 +1,141 @@ | |||
1 | + | import json | |
2 | + | import random | |
3 | + | import threading | |
4 | + | import time | |
5 | + | import traceback | |
6 | + | ||
7 | + | import redis | |
8 | + | import requests | |
9 | + | from flask import Flask, jsonify, request | |
10 | + | from flask_caching import Cache | |
11 | + | ||
12 | + | redis_conn = redis.Redis(host='localhost', port=6379, db=2) | |
13 | + | redis_conn.set('last_updated', 0) | |
14 | + | redis_conn.set('fetching_new_data', 0) | |
15 | + | if not redis_conn.hget('nodes', 'nodes'): | |
16 | + | redis_conn.hset('nodes', 'nodes', json.dumps({'data': {'nodes': []}})) | |
17 | + | ||
18 | + | headers = { | |
19 | + | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0', | |
20 | + | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', | |
21 | + | 'Accept-Language': 'en-US,en;q=0.5', | |
22 | + | 'Accept-Encoding': 'gzip;q=0,deflate;q=0', | |
23 | + | 'DNT': '1', | |
24 | + | 'Connection': 'keep-alive', | |
25 | + | 'Upgrade-Insecure-Requests': '1', | |
26 | + | 'Sec-Fetch-Dest': 'document', | |
27 | + | 'Sec-Fetch-Mode': 'navigate', | |
28 | + | 'Sec-Fetch-Site': 'cross-site', | |
29 | + | 'Pragma': 'no-cache', | |
30 | + | 'Cache-Control': 'no-cache', | |
31 | + | 'TE': 'trailers', | |
32 | + | } | |
33 | + | ||
34 | + | ||
35 | + | def update_node_cache(): | |
36 | + | # Semaphore | |
37 | + | if bool(int(redis_conn.get('fetching_new_data').decode())): | |
38 | + | return | |
39 | + | redis_conn.set('fetching_new_data', 1) | |
40 | + | print('Fetching new data...') | |
41 | + | start = time.time() | |
42 | + | try: | |
43 | + | r = requests.get('https://api.chub.ai/search?search=&first=5000000&min_tokens=50&nsfw=true', headers=headers, timeout=120, proxies={'http': 'http://172.0.4.7:9000', 'https': 'http://172.0.4.7:9000'}) | |
44 | + | j = r.json() | |
45 | + | j['timestamp'] = int(time.time()) | |
46 | + | redis_conn.set('node_count', len(j['data']['nodes'])) | |
47 | + | redis_conn.hset('nodes', 'nodes', json.dumps(j)) | |
48 | + | except: | |
49 | + | traceback.print_exc() | |
50 | + | finally: | |
51 | + | redis_conn.set('fetching_new_data', 0) | |
52 | + | print('Finished fetching new data in', time.time() - start) | |
53 | + | ||
54 | + | ||
55 | + | app = Flask(__name__) | |
56 | + | cache = Cache(app, config={'CACHE_TYPE': 'redis', 'CACHE_REDIS_URL': 'redis://localhost:6379/1', 'CACHE_KEY_PREFIX': 'chub_random__'}) | |
57 | + | cache.clear() | |
58 | + | ||
59 | + | ||
60 | + | @cache.memoize(timeout=900) | |
61 | + | def cached_node_json(): | |
62 | + | return json.loads(redis_conn.hget('nodes', 'nodes').decode()) | |
63 | + | ||
64 | + | ||
65 | + | def get_nodes(): | |
66 | + | last_updated = int(redis_conn.get('last_updated')) | |
67 | + | if time.time() > last_updated: | |
68 | + | threading.Thread(target=update_node_cache).start() | |
69 | + | count_raw = redis_conn.get('node_count') | |
70 | + | if not count_raw or not int(count_raw): | |
71 | + | # Return dummy data if there isn't anything loaded yet. | |
72 | + | # This avoids caching the empty data. | |
73 | + | return {'data': {'nodes': []}} | |
74 | + | else: | |
75 | + | # Cache and return data. | |
76 | + | return cached_node_json() | |
77 | + | ||
78 | + | ||
79 | + | @cache.memoize(timeout=900) | |
80 | + | def get_items_by_topics(topics: list): | |
81 | + | count_raw = redis_conn.get('node_count') | |
82 | + | if not count_raw or not int(count_raw): | |
83 | + | return [] | |
84 | + | nodes = get_nodes()['data']['nodes'] | |
85 | + | result = [] | |
86 | + | for node in nodes: | |
87 | + | if set(topics).issubset(set(node['topics'])): | |
88 | + | result.append(node) | |
89 | + | return result | |
90 | + | ||
91 | + | ||
92 | + | n = get_nodes() | |
93 | + | redis_conn.set('node_count', len(n['data']['nodes'])) | |
94 | + | ||
95 | + | ||
96 | + | @cache.cached(timeout=900) | |
97 | + | @app.route('/api/characters', methods=['GET']) | |
98 | + | def all_characters(): | |
99 | + | c = get_nodes() | |
100 | + | if not c: | |
101 | + | r = jsonify({'error': 'Something went wrong fetching response'}) | |
102 | + | r.headers['Access-Control-Allow-Origin'] = '*' | |
103 | + | return r, 500 | |
104 | + | else: | |
105 | + | r = jsonify(c) | |
106 | + | r.headers['Cache-Control'] = 'public, max-age=900' | |
107 | + | r.headers['Access-Control-Allow-Origin'] = '*' | |
108 | + | return r | |
109 | + | ||
110 | + | ||
111 | + | @app.route('/api/characters/random', methods=['GET']) | |
112 | + | def random_character(): | |
113 | + | c = get_nodes() | |
114 | + | tags_arg = request.args.get('tags') | |
115 | + | response_data = {} | |
116 | + | if not tags_arg: | |
117 | + | if not c: | |
118 | + | r = jsonify({'error': 'Something went wrong'}) | |
119 | + | r.headers['Access-Control-Allow-Origin'] = '*' | |
120 | + | return r, 500 | |
121 | + | elif len(c['data']['nodes']): | |
122 | + | count = int(redis_conn.get('node_count').decode()) | |
123 | + | i = random.randint(0, count) | |
124 | + | response_data = c['data']['nodes'][i] | |
125 | + | response_data['timestamp'] = c['timestamp'] | |
126 | + | else: | |
127 | + | tags = tags_arg.split(',') | |
128 | + | matches = get_items_by_topics(tags) | |
129 | + | if len(matches): | |
130 | + | response_data = random.choice(matches) | |
131 | + | response_data['timestamp'] = c['timestamp'] | |
132 | + | else: | |
133 | + | response_data = [] | |
134 | + | r = jsonify(response_data) | |
135 | + | r.headers['Access-Control-Allow-Origin'] = '*' | |
136 | + | r.headers['Cache-Control'] = 'no-store' | |
137 | + | return r | |
138 | + | ||
139 | + | ||
140 | + | if __name__ == '__main__': | |
141 | + | app.run(debug=True, host='0.0.0.0') |
cyberes revised this gist . Go to revision
1 file changed, 2 insertions, 11 deletions
index.html
@@ -26,10 +26,6 @@ | |||
26 | 26 | </div> | |
27 | 27 | </div> | |
28 | 28 | ||
29 | - | <div id="content"> | |
30 | - | ||
31 | - | </div> | |
32 | - | ||
33 | 29 | <script> | |
34 | 30 | function eraseItem() { | |
35 | 31 | document.getElementById("avatar").innerText = ''; | |
@@ -47,10 +43,9 @@ | |||
47 | 43 | document.getElementById('newItemButton').disabled = true; | |
48 | 44 | eraseItem() | |
49 | 45 | const tagsText = document.getElementById('topicsInput').value.replace(" ", ""); | |
50 | - | const tagsList = tagsText.split(','); | |
51 | - | ||
52 | 46 | let apiURL = 'https://chub-archive.evulid.cc/api/chub/characters/random' | |
53 | - | if (tagsList.length > 0) { | |
47 | + | if (tagsText.length > 0) { | |
48 | + | const tagsList = tagsText.split(','); | |
54 | 49 | apiURL = apiURL + '?tags=' + tagsList.join(',') | |
55 | 50 | } | |
56 | 51 | ||
@@ -160,19 +155,15 @@ | |||
160 | 155 | text-align: center; | |
161 | 156 | } | |
162 | 157 | ||
163 | - | /* or, for legacy browsers */ | |
164 | - | ||
165 | 158 | ::-webkit-input-placeholder { | |
166 | 159 | text-align: center; | |
167 | 160 | } | |
168 | 161 | ||
169 | 162 | :-moz-placeholder { | |
170 | - | /* Firefox 18- */ | |
171 | 163 | text-align: center; | |
172 | 164 | } | |
173 | 165 | ||
174 | 166 | ::-moz-placeholder { | |
175 | - | /* Firefox 19+ */ | |
176 | 167 | text-align: center; | |
177 | 168 | } | |
178 | 169 |
cyberes revised this gist . Go to revision
1 file changed, 188 insertions
index.html(file created)
@@ -0,0 +1,188 @@ | |||
1 | + | <!DOCTYPE html> | |
2 | + | <html lang="en"> | |
3 | + | ||
4 | + | <head> | |
5 | + | <title>Random Chub Characters</title> | |
6 | + | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
7 | + | </head> | |
8 | + | ||
9 | + | <body> | |
10 | + | <button id="newItemButton" style="width:100%;margin-bottom:10px;">Get New Item</button> | |
11 | + | <input id="topicsInput" type="text" placeholder="tags" style="width:45%;margin:auto;display:block;"> | |
12 | + | <br><br> | |
13 | + | <div class="flex-container"> | |
14 | + | <div style="min-width: 25%;"> | |
15 | + | <div id="avatar"></div> | |
16 | + | </div> | |
17 | + | <div class="card-desc" style="width: 100%;"> | |
18 | + | <div id="name"></div> | |
19 | + | <div id="tagline"></div> | |
20 | + | <div id="desc">Loading...</div> | |
21 | + | <div id="tags"></div> | |
22 | + | <div id="tokens"></div> | |
23 | + | <div id="author"></div> | |
24 | + | <div id="date"></div> | |
25 | + | <div id="downloads"></div> | |
26 | + | </div> | |
27 | + | </div> | |
28 | + | ||
29 | + | <div id="content"> | |
30 | + | ||
31 | + | </div> | |
32 | + | ||
33 | + | <script> | |
34 | + | function eraseItem() { | |
35 | + | document.getElementById("avatar").innerText = ''; | |
36 | + | document.getElementById("name").innerText = ''; | |
37 | + | document.getElementById("tagline").innerText = ''; | |
38 | + | document.getElementById("desc").innerText = 'Loading...'; | |
39 | + | document.getElementById("tags").innerText = ''; | |
40 | + | document.getElementById("tokens").innerText = ''; | |
41 | + | document.getElementById("author").innerText = ''; | |
42 | + | document.getElementById("date").innerText = ''; | |
43 | + | document.getElementById("downloads").innerText = ''; | |
44 | + | } | |
45 | + | ||
46 | + | function displayRandomItem() { | |
47 | + | document.getElementById('newItemButton').disabled = true; | |
48 | + | eraseItem() | |
49 | + | const tagsText = document.getElementById('topicsInput').value.replace(" ", ""); | |
50 | + | const tagsList = tagsText.split(','); | |
51 | + | ||
52 | + | let apiURL = 'https://chub-archive.evulid.cc/api/chub/characters/random' | |
53 | + | if (tagsList.length > 0) { | |
54 | + | apiURL = apiURL + '?tags=' + tagsList.join(',') | |
55 | + | } | |
56 | + | ||
57 | + | fetch(apiURL) | |
58 | + | .then(response => response.json()) | |
59 | + | .then(data => { | |
60 | + | if (data.name === undefined) { | |
61 | + | document.getElementById("name").innerHTML = "No cards found"; | |
62 | + | document.getElementById("desc").innerText = ''; | |
63 | + | } else { | |
64 | + | document.getElementById("avatar").innerHTML = `<img src="https://avatars.charhub.io/avatars/${data.fullPath}/avatar.webp?size=0.07711525216207438" alt="">`; | |
65 | + | document.getElementById("name").innerHTML = `<a href="https://www.chub.ai/characters/${data.fullPath}" target="_blank">${data.name}</a>`; | |
66 | + | document.getElementById("tagline").innerText = data.tagline; | |
67 | + | document.getElementById("desc").innerText = data.description; | |
68 | + | document.getElementById("tags").innerText = data.topics.join(', '); | |
69 | + | document.getElementById("tokens").innerText = `Approximately ${data.nTokens} tokens`; | |
70 | + | document.getElementById("author").innerHTML = `<a href="https://www.chub.ai/users/${data.fullPath.split('/')[0]}" target="_blank">${data.fullPath.split('/')[0]}</a>`; | |
71 | + | document.getElementById("date").innerText = data.lastActivityAt; | |
72 | + | document.getElementById("downloads").innerText = `${data.starCount} Downloads`; | |
73 | + | } | |
74 | + | document.getElementById('newItemButton').disabled = false; | |
75 | + | }) | |
76 | + | .catch(error => console.error(error)); | |
77 | + | } | |
78 | + | ||
79 | + | window.onload = displayRandomItem; | |
80 | + | document.getElementById('topicsInput').addEventListener("keydown", (event) => { | |
81 | + | if (event.key === "Enter") { | |
82 | + | displayRandomItem(); | |
83 | + | } | |
84 | + | }); | |
85 | + | document.getElementById('newItemButton').addEventListener("click", displayRandomItem); | |
86 | + | </script> | |
87 | + | </body> | |
88 | + | ||
89 | + | <style> | |
90 | + | .flex-container { | |
91 | + | display: flex; | |
92 | + | flex-wrap: nowrap; | |
93 | + | background-color: #7C0818; | |
94 | + | } | |
95 | + | ||
96 | + | .flex-container>div { | |
97 | + | background-color: #f1f1f1; | |
98 | + | margin: 10px; | |
99 | + | padding: 10px; | |
100 | + | } | |
101 | + | ||
102 | + | ||
103 | + | .card-desc { | |
104 | + | font-size: 12pt; | |
105 | + | word-wrap: normal; | |
106 | + | } | |
107 | + | ||
108 | + | #newItemButton { | |
109 | + | background-color: #7C0818; | |
110 | + | border-radius: 10px; | |
111 | + | color: white; | |
112 | + | font-weight: bold; | |
113 | + | cursor: pointer; | |
114 | + | font-size: 20pt; | |
115 | + | } | |
116 | + | ||
117 | + | #newItemButton:disabled { | |
118 | + | cursor: not-allowed; | |
119 | + | } | |
120 | + | ||
121 | + | #avatar>img { | |
122 | + | max-width: 100%; | |
123 | + | display: block; | |
124 | + | margin: auto; | |
125 | + | } | |
126 | + | ||
127 | + | #name { | |
128 | + | font-weight: bold; | |
129 | + | font-size: 20pt; | |
130 | + | margin-bottom: 10px; | |
131 | + | } | |
132 | + | ||
133 | + | /*#name > a, #name > a:visited {*/ | |
134 | + | /* color: black;*/ | |
135 | + | /*}*/ | |
136 | + | ||
137 | + | #tagline { | |
138 | + | font-style: italic; | |
139 | + | margin-bottom: 20px; | |
140 | + | } | |
141 | + | ||
142 | + | #desc { | |
143 | + | padding: 5px; | |
144 | + | border: 1px solid black; | |
145 | + | margin-bottom: 20px; | |
146 | + | } | |
147 | + | ||
148 | + | #tags { | |
149 | + | font-style: italic; | |
150 | + | margin-bottom: 10px; | |
151 | + | } | |
152 | + | ||
153 | + | #tokens, | |
154 | + | #author, | |
155 | + | #date { | |
156 | + | margin-bottom: 5px; | |
157 | + | } | |
158 | + | ||
159 | + | ::placeholder { | |
160 | + | text-align: center; | |
161 | + | } | |
162 | + | ||
163 | + | /* or, for legacy browsers */ | |
164 | + | ||
165 | + | ::-webkit-input-placeholder { | |
166 | + | text-align: center; | |
167 | + | } | |
168 | + | ||
169 | + | :-moz-placeholder { | |
170 | + | /* Firefox 18- */ | |
171 | + | text-align: center; | |
172 | + | } | |
173 | + | ||
174 | + | ::-moz-placeholder { | |
175 | + | /* Firefox 19+ */ | |
176 | + | text-align: center; | |
177 | + | } | |
178 | + | ||
179 | + | :-ms-input-placeholder { | |
180 | + | text-align: center; | |
181 | + | } | |
182 | + | ||
183 | + | input { | |
184 | + | text-align: center; | |
185 | + | } | |
186 | + | </style> | |
187 | + | ||
188 | + | </html> |