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