Compare commits
3 commits
f2a6d186c8
...
1cd3a48324
Author | SHA1 | Date | |
---|---|---|---|
1cd3a48324 | |||
3fbb7580c7 | |||
84516d3aeb |
|
@ -6,7 +6,7 @@ import json
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Flask, render_template, jsonify, request, abort
|
from flask import Flask, render_template, jsonify, request, abort
|
||||||
from flask.helpers import send_file
|
from flask.helpers import send_file, send_from_directory
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from core_client import Client
|
from core_client import Client
|
||||||
from ffmpeg import FFmpeg, Progress
|
from ffmpeg import FFmpeg, Progress
|
||||||
|
@ -312,9 +312,15 @@ scheduler.get_job('core_api_sync').modify(next_run_time=datetime.now())
|
||||||
# Start the scheduler
|
# Start the scheduler
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
# Flask API
|
## Flask
|
||||||
|
# Frontend
|
||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'])
|
||||||
def root_route():
|
def root_route():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
# API
|
||||||
|
@app.route('/about', methods=['GET'])
|
||||||
|
def about_route():
|
||||||
about_json = { 'about': 'DeflaxTV API' }
|
about_json = { 'about': 'DeflaxTV API' }
|
||||||
return jsonify(about_json)
|
return jsonify(about_json)
|
||||||
|
|
||||||
|
@ -335,13 +341,6 @@ def database_route():
|
||||||
return jsonify(database)
|
return jsonify(database)
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
@app.route("/img/<file_name>", methods=['GET'])
|
|
||||||
def img_route(file_name):
|
|
||||||
reqfile = f'./img/{file_name}'
|
|
||||||
if not os.path.exists(reqfile):
|
|
||||||
abort(404)
|
|
||||||
return send_file(reqfile, mimetype='image/png')
|
|
||||||
|
|
||||||
@app.route("/thumb/<file_name>", methods=['GET'])
|
@app.route("/thumb/<file_name>", methods=['GET'])
|
||||||
def thumb_route(file_name):
|
def thumb_route(file_name):
|
||||||
reqfile = f'{rec_path}/thumb/{file_name}'
|
reqfile = f'{rec_path}/thumb/{file_name}'
|
||||||
|
@ -366,7 +365,7 @@ def video_download_route(file_name):
|
||||||
logger_content.warning(str(reqfile) + ' download')
|
logger_content.warning(str(reqfile) + ' download')
|
||||||
return send_file(reqfile, as_attachment=True, download_name=file_name)
|
return send_file(reqfile, as_attachment=True, download_name=file_name)
|
||||||
|
|
||||||
@app.route('/video/watch/<file_name_no_extension>', methods=['GET'])
|
@app.route("/video/watch/<file_name_no_extension>", methods=['GET'])
|
||||||
def video_watch_route(file_name_no_extension):
|
def video_watch_route(file_name_no_extension):
|
||||||
video_file = f'{file_name_no_extension}.mp4'
|
video_file = f'{file_name_no_extension}.mp4'
|
||||||
thumb_file = f'{file_name_no_extension}.png'
|
thumb_file = f'{file_name_no_extension}.png'
|
||||||
|
|
36
src/api/static/css/root.css
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background: black;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font: bold 18px sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255,255,255,.55) !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
font: bold 18px sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: rgb(132,4,217) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid {
|
||||||
|
scroll-margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.violet {
|
||||||
|
color: rgb(132,4,217) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
BIN
src/api/static/images/favicon.ico
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
src/api/static/images/forgejo-96.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/api/static/images/freeravemini.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src/api/static/images/icons8-discord-96.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/api/static/images/icons8-facebook-96.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/api/static/images/icons8-instagram-96.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/api/static/images/icons8-soundcloud-96.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
src/api/static/images/icons8-twitch-96.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
src/api/static/images/icons8-youtube-96.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/api/static/images/jtlogo.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
BIN
src/api/static/images/logo.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/api/static/images/odeala.jpg
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
src/api/static/images/odealo.jpg
Normal file
After Width: | Height: | Size: 702 KiB |
BIN
src/api/static/images/poster.png
Normal file
After Width: | Height: | Size: 3.1 MiB |
338
src/api/templates/index.html
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta property="og:title" content=deflax.net>
|
||||||
|
<meta property="og:site_name" content="▷">
|
||||||
|
<meta property="og:url" content=https://deflax.net>
|
||||||
|
<meta property="og:description" content="The landing page of the flaxnet">
|
||||||
|
<meta property="og:type" content=product>
|
||||||
|
<meta property="og:image" content=https://deflax.net/static/images/logo.png>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<title>▷ deflax.net</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="static/css/root.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.min.css" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/plyr/dist/plyr.min.css">
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/plyr/dist/plyr.min.js" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hls.js/dist/hls.min.js" crossorigin="anonymous"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="#" style="padding-left: 8.5%;">
|
||||||
|
<img src="static/images/logo.png" alt="" width="40" height="40" class="d-inline-block align-text-center">
|
||||||
|
deflax.net
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse text-right" id="navbarNav" style="padding-right: 8.5%;">
|
||||||
|
<ul class="navbar-nav ms-auto flex-nowrap">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#stream">Stream <i class="fas fa-satellite-dish" aria-hidden="true"></i></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#playlists">Playlists <i class="fa fa-hard-drive" aria-hidden="true"></i></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#archive">Archive <i class="fa fa-hard-drive" aria-hidden="true"></i></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="content-container bg-dark text-white">
|
||||||
|
<div id="stream" class="container-fluid p-3 bg-dark text-white">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-1">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-10">
|
||||||
|
<div class="content">
|
||||||
|
<video controls crossorigin playsinline poster="static/images/poster.png">
|
||||||
|
</video>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const video = document.querySelector("video");
|
||||||
|
const source = 'https://stream.deflax.net/memfs/f2844c7c-e86c-4f0a-afb6-a1b0e25a5071.m3u8';
|
||||||
|
const playhead_url = "https://tv.deflax.net/playhead";
|
||||||
|
|
||||||
|
// For more options, see: https://github.com/sampotts/plyr/#options
|
||||||
|
// captions.update is required for captions to work with hls.js
|
||||||
|
const defaultOptions = {};
|
||||||
|
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls();
|
||||||
|
|
||||||
|
// Function to fetch and process JSON from the API
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
// Fetch data from the API
|
||||||
|
const response = await fetch(playhead_url);
|
||||||
|
|
||||||
|
// Check if the response is successful (status code 200)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON from the response
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Now 'data' contains the parsed JSON
|
||||||
|
console.log("Fetched JSON data:", data);
|
||||||
|
|
||||||
|
const oldsrc = hls.url;
|
||||||
|
const newsrc = data.head;
|
||||||
|
|
||||||
|
if (newsrc === oldsrc) {
|
||||||
|
//console.log(oldsrc);
|
||||||
|
//console.log(newsrc);
|
||||||
|
} else {
|
||||||
|
console.log('playhead: switching to ' + newsrc)
|
||||||
|
hls.loadSource(newsrc);
|
||||||
|
video.load(); // Reload the video element to apply the new source
|
||||||
|
video.play().catch(error => {
|
||||||
|
console.error('Autoplay was prevented:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
// Set the interval for periodic execution (e.g., every 5 seconds)
|
||||||
|
const interval = 4200; // in milliseconds
|
||||||
|
setInterval(fetchData, interval);
|
||||||
|
|
||||||
|
// Attach Hls.js to the video element
|
||||||
|
hls.loadSource(source);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
|
||||||
|
// From the m3u8 playlist, hls parses the manifest and returns
|
||||||
|
// all available video qualities. This is important; in this approach,
|
||||||
|
// we will have one source on the Plyr player.
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
||||||
|
console.log('hls: manifest parsed');
|
||||||
|
|
||||||
|
// Transform available levels into an array of integers (height values).
|
||||||
|
const availableQualities = hls.levels.map((l) => l.height)
|
||||||
|
availableQualities.unshift(0); //prepend 0 to quality array
|
||||||
|
|
||||||
|
// Add new qualities to option
|
||||||
|
defaultOptions.quality = {
|
||||||
|
default: 0, //Default - AUTO
|
||||||
|
options: availableQualities,
|
||||||
|
forced: true,
|
||||||
|
onChange: (e) => updateQuality(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Auto Label
|
||||||
|
defaultOptions.i18n = {
|
||||||
|
qualityLabel: {
|
||||||
|
0: 'Auto',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) {
|
||||||
|
console.log('hls: level switched');
|
||||||
|
var span = document.querySelector(".plyr__menu__container [data-plyr='quality'][value='0'] span")
|
||||||
|
if (hls.autoLevelEnabled) {
|
||||||
|
span.innerHTML = `AUTO (${hls.levels[data.level].height}p)`
|
||||||
|
} else {
|
||||||
|
span.innerHTML = `AUTO`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize new Plyr player with quality options
|
||||||
|
const player = new Plyr(video, defaultOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.hls = hls;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Hls.js is not supported, fallback to standard video element
|
||||||
|
// Function to fetch and process JSON from the API
|
||||||
|
async function fetchInitData() {
|
||||||
|
try {
|
||||||
|
// Fetch data from the API
|
||||||
|
const response = await fetch(playhead_url);
|
||||||
|
|
||||||
|
// Check if the response is successful (status code 200)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON from the response
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Now 'data' contains the parsed JSON
|
||||||
|
console.log("Fetched JSON data:", data);
|
||||||
|
|
||||||
|
video.src = data.head;
|
||||||
|
const player = new Plyr(video, defaultOptions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchInitData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuality(newQuality) {
|
||||||
|
if (newQuality === 0) {
|
||||||
|
window.hls.currentLevel = -1; //Enable AUTO quality if option.value = 0
|
||||||
|
} else {
|
||||||
|
window.hls.levels.forEach((level, levelIndex) => {
|
||||||
|
if (level.height === newQuality) {
|
||||||
|
console.log("hls: Found quality match with " + newQuality);
|
||||||
|
window.hls.currentLevel = levelIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-container bg-dark text-white">
|
||||||
|
<div id="terminal" class="container-fluid p-3 bg-dark text-white ">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-1 d-none d-md-block">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-10 d-none d-md-block">
|
||||||
|
<div class="content">
|
||||||
|
<widgetbot
|
||||||
|
server="803220836407443506"
|
||||||
|
channel="1204242382489522250"
|
||||||
|
width="100%"
|
||||||
|
height="600"
|
||||||
|
></widgetbot>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@widgetbot/html-embed"></script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-1 d-none d-md-block">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-container bg-dark text-white">
|
||||||
|
<div id="playlists" class="container-fluid p-3 bg-dark text-white">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1 d-none d-md-block">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-5 d-none d-md-block">
|
||||||
|
<div class="content">
|
||||||
|
<h2>PVC playlist</h2>
|
||||||
|
<iframe width="100%" height="480" src="https://www.youtube.com/embed/videoseries?si=RdMNZq4hYudlCW37&list=PLVB9xwlHyW_LhxubZ9HL8wzx1mmDNBj0z" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-5 d-none d-md-block">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Digital playlist and stuff</h2>
|
||||||
|
<iframe width="100%" height="480" src="https://www.youtube.com/embed/videoseries?si=SPAsQfxoya_Jeurd&list=PLVB9xwlHyW_LWz1KEmhxXhGbypcpcdHM3" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-none d-md-block">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-container bg-dark text-white">
|
||||||
|
<div id="archive" class="container-fluid p-3 bg-dark text-white">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1 d-none d-md-block">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 d-none d-md-block">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Video Archive</h2>
|
||||||
|
<iframe width="100%" height="700" src="https://tv.deflax.net/gallery" title="Video Archive" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-none d-md-block">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-container bg-dark text-white">
|
||||||
|
<div id="info" class="container-fluid p-3 bg-dark text-white">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xs-12 col-md-2">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Upstreams</h2>
|
||||||
|
<p><a href='https://youtube.com/@daniel.deflax' target=_blank><img src="static/images/icons8-youtube-96.png"></a></p>
|
||||||
|
<p><a href='https://www.twitch.tv/deflaxtv' target_blank><img src="static/images/icons8-twitch-96.png"></a></p>
|
||||||
|
<p><a href='https://soundcloud.com/deflax' target=_blank><img src="static/images/icons8-soundcloud-96.png"></a></p>
|
||||||
|
<p><a href='https://facebook.com/daniel.default' target=_blank><img src="static/images/icons8-facebook-96.png"></a></p>
|
||||||
|
<p><a href='https://instagram.com/daniel.deflax' target=_blank><img src="static/images/icons8-instagram-96.png"></a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-2">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Backroom</h2>
|
||||||
|
<p><a href='https://discord.com/invite/KKGtn6GCE3' target=_blank><img src="static/images/icons8-discord-96.png"></a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Friends</h2>
|
||||||
|
<p><a href='https://jungletrain.net/' target=_blank><img src="static/images/jtlogo.png" height="64"></a></p>
|
||||||
|
<p><a href='http://freerave.cz/' target=_blank><img src="static/images/freeravemini.png" height="64"></a></p>
|
||||||
|
<p><a href='http://iwayhigh.net/' target=_blank>|iway|High</a></p>
|
||||||
|
<p><a href='https://anima.sknt.ru/' target=_blank>Anima Amoris</a></p>
|
||||||
|
<p>You! 🙃</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-3">
|
||||||
|
<div class="content">
|
||||||
|
<h2>About</h2>
|
||||||
|
<pre>
|
||||||
|
_______________________________
|
||||||
|
/|o............................o|
|
||||||
|
| |: .. flax network .. .. :|
|
||||||
|
| |: ... . ... 2024 .... :|
|
||||||
|
| |: ,-. _____ ,-. :|
|
||||||
|
| |: ( `)) [_____] ( `)) :|
|
||||||
|
|v|: `-` ' ' ' `-` :|
|
||||||
|
|||: ,______________. :|
|
||||||
|
|||....../::::o::::::o::::\......|
|
||||||
|
|^|o..../:::O::::::::::O:::\....o|
|
||||||
|
|/`----/--------------------`----|
|
||||||
|
`.____/ /====/ /=//=/ /====/_____/
|
||||||
|
`--------------------'
|
||||||
|
</pre>
|
||||||
|
<p>Icons by <a target="_blank" href="https://icons8.com">Icons8</a></p>
|
||||||
|
<p>Everthying you do is a <a href='https://rareboc.org/' target=_blank>balloon</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-xs-12 col-md-1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
2
src/api/templates/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: GPTBot
|
||||||
|
Disallow: /
|