Last active 1705888099

index.html Raw
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
1import json
2import random
3import threading
4import time
5import traceback
6
7import redis
8import requests
9from flask import Flask, jsonify, request
10from flask_caching import Cache
11
12redis_conn = redis.Redis(host='localhost', port=6379, db=2)
13redis_conn.set('last_updated', 0)
14redis_conn.set('fetching_new_data', 0)
15if not redis_conn.hget('nodes', 'nodes'):
16 redis_conn.hset('nodes', 'nodes', json.dumps({'data': {'nodes': []}}))
17
18headers = {
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
35def 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
55app = Flask(__name__)
56cache = Cache(app, config={'CACHE_TYPE': 'redis', 'CACHE_REDIS_URL': 'redis://localhost:6379/1', 'CACHE_KEY_PREFIX': 'chub_random__'})
57cache.clear()
58
59
60@cache.memoize(timeout=900)
61def cached_node_json():
62 return json.loads(redis_conn.hget('nodes', 'nodes').decode())
63
64
65def 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)
80def 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
92n = get_nodes()
93redis_conn.set('node_count', len(n['data']['nodes']))
94
95
96@cache.cached(timeout=900)
97@app.route('/api/characters', methods=['GET'])
98def 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'])
112def 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
142if __name__ == '__main__':
143 app.run(debug=True, host='0.0.0.0')
144