--- /dev/null
+#! .venv/bin/python3
+
+from flask import Flask, Response, request, render_template, redirect, send_from_directory, url_for
+from flask_httpauth import HTTPBasicAuth
+from werkzeug.security import generate_password_hash, check_password_hash
+from multiprocessing import Value
+from os import listdir
+import sqlite3
+import random
+import json
+import os.path
+
+app = Flask(__name__)
+auth = HTTPBasicAuth()
+db_keys = ["title", "album", "artist", "year", "link", "info", "category", "playing", "intime", "outtime"]
+allowed_audio = ["mp3", "wav", "ogg", "flac"]
+allowed_image = ["png", "jpg", "jpeg"]
+
+current_track_id = Value("I") # This is the global track ID
+with current_track_id.get_lock():
+ current_track_id.value = 1
+
+audio_path = "../audio/"
+cover_path = "../cover/"
+icon_path = "../icon/"
+upload_path = "../upload/"
+
+def check_files(track_id):
+ return {
+ "track_path": url_for("get_audio", track_id=track_id),
+ "track_exists": os.path.isfile(f"{audio_path}{track_id}.mp3"),
+ "front_anno_path": url_for("get_audio", track_id=track_id, position="front"),
+ "front_anno_exists": os.path.isfile(f"{audio_path}{track_id}-front.mp3"),
+ "back_anno_path": url_for("get_audio", track_id=track_id, position="back"),
+ "back_anno_exists": os.path.isfile(f"{audio_path}{track_id}-back.mp3"),
+ "cover_path": url_for("get_cover", track_id=track_id),
+ "cover_exists": os.path.isfile(f"{cover_path}{track_id}.png"),
+ "icon_path": url_for("get_icon", track_id=track_id)
+ }
+
+with open("../secrets.json", "r") as f:
+ users = json.loads(f.read())
+users = {k: generate_password_hash(v) for k: v in users}
+
+@auth.verify_password
+def verify_password(username, password):
+ if username in users and \
+ check_password_hash(users.get(username), password):
+ return username
+
+# Homepage
+@app.route("/")
+def homepage():
+ page = render_template("home.html")
+ return Response(page, mimetype="text/html")
+
+# Admin pannel
+@app.route("/admin", methods=['POST', 'GET'])
+@auth.login_required
+def admin():
+ if request.method == "POST":
+ con = sqlite3.connect("../../metadata.db")
+ cur = con.cursor()
+
+ try: commit_id = int(request.form["id"])
+ except: commit_id = 0
+
+ if "playing" in request.form:
+ playing = "true"
+ else:
+ playing = "false"
+
+ print(playing)
+
+ # Get the new track id if neccicary
+ if commit_id == 0:
+ tracks = cur.execute(f"SELECT id FROM critters").fetchall()
+ tracks = [track_id[0] for track_id in tracks]
+ commit_id = 1
+ while commit_id in tracks:
+ commit_id += 1
+
+ cur.execute(f"""
+ INSERT INTO critters
+ (id, title, album, artist, year, link, info, category, playing, intime, outtime)
+ VALUES (
+ {commit_id},
+ '{request.form["title"]}',
+ '{request.form["album"]}',
+ '{request.form["artist"]}',
+ '{request.form["year"]}',
+ '{request.form["link"]}',
+ '{request.form["info"]}',
+ '{request.form["category"]}',
+ '{playing}',
+ '{request.form["intime"]}',
+ '{request.form["outtime"]}'
+ );
+ """)
+ con.commit()
+
+ else:
+ if "delete" in request.form:
+ cur.execute(f"DELETE FROM critters WHERE id={request.form["id"]};")
+ con.commit()
+
+ con.close()
+ return ""
+
+ else:
+ cur.execute(f"""
+ UPDATE critters SET
+ title = '{request.form["title"]}',
+ album = '{request.form["album"]}',
+ artist = '{request.form["artist"]}',
+ year = '{request.form["year"]}',
+ link = '{request.form["link"]}',
+ info = '{request.form["info"]}',
+ category = '{request.form["category"]}',
+ playing = '{playing}',
+ intime = '{request.form["intime"]}',
+ outtime = '{request.form["outtime"]}'
+ WHERE id = {commit_id};
+ """)
+ con.commit()
+
+ if "audio" in request.files:
+ upload = request.files["audio"]
+ if upload.filename != "":
+ extention = upload.filename.rsplit('.', 1)[1].lower()
+ if extention in allowed_audio:
+ file_path = f"{upload_path}{commit_id}.{extention}"
+ upload.save(file_path)
+ os.system(f"sox {file_path} -r 22050 {audio_path}{commit_id}.mp3 &")
+
+ if "front" in request.files:
+ upload = request.files["front"]
+ if upload.filename != "":
+ extention = upload.filename.rsplit('.', 1)[1].lower()
+ if extention in allowed_audio:
+ file_path = f"{upload_path}{commit_id}-front.{extention}"
+ upload.save(file_path)
+ os.system(f"sox {file_path} -r 22050 {audio_path}{commit_id}-front.mp3 &")
+
+ if "back" in request.files:
+ upload = request.files["back"]
+ if upload.filename != "":
+ extention = upload.filename.rsplit('.', 1)[1].lower()
+ if extention in allowed_audio:
+ file_path = f"{upload_path}{commit_id}-back.{extention}"
+ upload.save(file_path)
+ os.system(f"sox {file_path} -r 22050 {audio_path}{commit_id}-back.mp3 &")
+
+ if "cover" in request.files:
+ upload = request.files["cover"]
+ if upload.filename != "":
+ extention = upload.filename.rsplit('.')[-1].lower()
+ if extention in allowed_image:
+ file_path = f"{upload_path}{commit_id}.{extention}"
+ upload.save(file_path)
+ os.system(f"convert {file_path} -scale 32x32! {icon_path}{commit_id}.png &")
+ os.system(f"convert {file_path} -scale 600x600! {cover_path}{commit_id}.png &")
+
+ page = render_template("admin.html", user = auth.current_user())
+ return Response(page, mimetype="text/html")
+
+# Get the next track
+@app.route("/api/next")
+@auth.login_required
+def set_current_id():
+ server_data = { # The value for each of the keys is the position in the database columns that peice of data can be found
+ "id": 0, # (Note 1)
+ "title": 1,
+ "artist": 3,
+ "intime": 9,
+ "outtime": 10
+ }
+
+ with current_track_id.get_lock(): # Lock for entire function
+ track_id = current_track_id.value
+
+ current_album = ""
+ current_artist = ""
+
+ con = sqlite3.connect("../../metadata.db")
+ cur = con.cursor()
+ track = cur.execute(f"SELECT album, artist FROM critters WHERE id={track_id}").fetchone()
+
+ if track is not None:
+ current_album, current_artist = track
+
+ category_secifier = ""
+ if "c" in request.args:
+ category = request.args.get("c")
+ category_secifier = f" AND category = '{category}'"
+
+ tracks = cur.execute(f"""
+ SELECT * FROM critters WHERE
+ playing = 'true' AND
+ id != {track_id} AND
+ album != '{current_album}' AND
+ artist != '{current_artist}'{category_secifier}
+ """).fetchall()
+
+ if tracks is not None:
+ target_index = random.randint(0, len(tracks)-1)
+
+ current_track_id.value = tracks[target_index][0]
+
+ for key, i in server_data.items():
+ server_data[key] = tracks[target_index][i] # See note 1
+
+ return Response(json.dumps(server_data), mimetype="text/json")
+
+@app.route("/api/get", methods=['GET'])
+def get_tracks():
+ track_data = {
+ "id": 0,
+ "title": "Unknown",
+ "album": "Unknown",
+ "artist": "Unknown",
+ "year": "Unknown",
+ "link": "https://treecritters.bandcamp.com",
+ "info": "No info",
+ "category":"None",
+ "playing":"false"
+ }
+ responce_data = [track_data]
+
+ con = sqlite3.connect("../../metadata.db")
+ cur = con.cursor()
+ tracks = cur.execute(f"SELECT * FROM critters").fetchall()
+ if tracks is not None:
+ responce_data = [{key: track[i] for (i, key) in enumerate(track_data)} | check_files(track[0]) for track in tracks]
+
+ return Response(json.dumps(responce_data), mimetype="text/json")
+
+# Call for getting single track details with all feilds in JSON
+@app.route("/api/<int:track_id>")
+def get_single_track(track_id):
+ if track_id == 0:
+ with current_track_id.get_lock():
+ track_id = current_track_id.value
+ con = sqlite3.connect("../../metadata.db")
+ cur = con.cursor()
+ track = cur.execute(f"SELECT * FROM critters WHERE id={track_id}").fetchone()
+ track_data = {
+ "id": 0,
+ "title": "Unknown",
+ "album": "Unknown",
+ "artist": "Unknown",
+ "year": "Unknown",
+ "link": "https://treecritters.bandcamp.com",
+ "info": "No info",
+ "category": "None",
+ "playing": "false",
+ }
+ cover_path = {"cover_path": url_for("get_cover", track_id=track_id)}
+ if track is not None:
+ for (i, key) in enumerate(track_data):
+ track_data[key] = track[i]
+ return Response(json.dumps(track_data | cover_path), mimetype="text/json")
+
+# Call for getting single track details with a single feild
+@app.route("/api/<int:track_id>.<string:feild>")
+def get_single_field(track_id, feild):
+ if track_id == 0:
+ with current_track_id.get_lock():
+ track_id = current_track_id.value
+ result = "Unknown"
+ if feild in db_keys:
+ con = sqlite3.connect("../../metadata.db")
+ cur = con.cursor()
+ track = cur.execute(f"SELECT {feild} FROM critters WHERE id={track_id}").fetchone()
+ if track is not None:
+ result = track[0]
+ if feild == "cover_path":
+ result = url_for("get_cover", track_id=track_id)
+
+ return Response(result, mimetype="text/plaintext")
+
+@app.route("/api/audio/<int:track_id>")
+@auth.login_required
+def get_audio(track_id):
+ if "position" in request.args:
+ position = request.args.get('position')
+ if position in ["front", "back"]:
+ return send_from_directory(audio_path, f"{track_id}-{position}.mp3")
+
+ return send_from_directory(audio_path, f"{track_id}.mp3")
+
+@app.route("/api/icon/<int:track_id>")
+@auth.login_required
+def get_icon(track_id):
+ if os.path.isfile(f"{icon_path}{track_id}.png"):
+ return send_from_directory(icon_path, f"{track_id}.png")
+ else:
+ return send_from_directory(icon_path, "default.png")
+
+@app.route("/api/cover/<int:track_id>")
+def get_cover(track_id):
+ if os.path.isfile(f"{cover_path}{track_id}.png"):
+ return send_from_directory(cover_path, f"{track_id}.png")
+ else:
+ return send_from_directory(cover_path, "default.png")
+
+if __name__ == "__main__":
+ app.run(host='127.0.0.1', port=5000, debug=True)
--- /dev/null
+CREATE TABLE critters (
+ id INTEGER PRIMARY KEY,
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ year TEXT,
+ link TEXT,
+ info TEXT,
+ category TEXT,
+ playing TEXT,
+ intime TEXT,
+ outtime TEXT
+);
--- /dev/null
+const goodSymbol = "✔";
+const badSymbol = "✘";
+
+function getTrackList () {
+ fetch("/api/get")
+ .then(data => data.text())
+ .then(data => JSON.parse(data))
+ .then(data => {
+ var html = `
+ <tr>
+ <th></th>
+ <th>#</th>
+ <th>Title</th>
+ <th>Album</th>
+ <th>Artist</th>
+ <th>Category</th>
+ <th>Playing</th>
+ <th>Track</th>
+ <th>Front</th>
+ <th>Back</th>
+ <th>Cover</th>
+ <th></th>
+ </tr>
+ `;
+ for (let track of data) {
+
+ if (track.playing == "true") {
+ trackPlaying = goodSymbol;
+ } else {
+ trackPlaying = badSymbol;
+ }
+
+ if (track.track_exists) {
+ trackExists = `<a href="${track.track_path}" target="_blank">${goodSymbol}</a>`;
+ } else {
+ trackExists = badSymbol;
+ }
+
+ if (track.front_anno_exists) {
+ frontAnnoExists = `<a href="${track.front_anno_path}" target="_blank">${goodSymbol}</a>`;
+ } else {
+ frontAnnoExists = badSymbol;
+ }
+
+ if (track.back_anno_exists) {
+ backAnnoExists = `<a href="${track.back_anno_path}" target="_blank">${goodSymbol}</a>`;
+ } else {
+ backAnnoExists = badSymbol;
+ }
+
+ if (track.cover_exists) {
+ coverExists = `<a href="${track.cover_path}" target="_blank">${goodSymbol}</a>`;
+ } else {
+ coverExists = badSymbol;
+ }
+
+ html += `
+ <tr id="track${track.id}">
+ <td><img class="smallIcon" src="${track.icon_path}" /></td>
+ <td>${track.id}</td>
+ <td>${track.title}</td>
+ <td>${track.album}</td>
+ <td>${track.artist}</td>
+ <td>${track.category}</td>
+ <td>${trackPlaying}</td>
+ <td>${trackExists}</td>
+ <td>${frontAnnoExists}</td>
+ <td>${backAnnoExists}</td>
+ <td>${coverExists}</td>
+ <td>
+ <input type="button" value="Edit track" onclick="editTrack(${track.id});" />
+ <input type="button" value="Delete track" onclick="deleteTrack(${track.id});" />
+ </td>
+ </tr>
+ `
+ };
+ return html
+ })
+ .then(data => document.getElementById("trackListFull").innerHTML = data);
+}
+
+function newTrack () {
+ const elements = document.getElementsByClassName("mainForm");
+ console.log(elements)
+ for (const e of elements) {
+ e.value = "";
+ }
+ document.getElementById("id").value = "New track";
+ document.getElementById("playing").checked = true;
+ document.getElementById("thumbnail").src = `/api/cover/0`;
+}
+
+function editTrack (id) {
+ document.getElementById("thumbnail").src = `/api/cover/${id}`
+ fetch(`/api/${id}`)
+ .then(data => data.text())
+ .then(data => JSON.parse(data))
+ .then(data => {
+ for (let x in data) {
+ document.getElementById(x).value = data[x];
+ }
+ if (data.playing == "true") {
+ document.getElementById("playing").checked = true;
+ } else {
+ document.getElementById("playing").checked = false;
+ }
+ });
+}
+
+function deleteTrack (id) {
+ const form = new FormData();
+ form.append("id", id);
+ form.append("delete", 1);
+
+ fetch("/admin", {
+ method: "POST",
+ body: form
+ })
+ .then(document.getElementById(`track${id}`).remove());
+}
+
+getTrackList();
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="1000"
+ height="1000"
+ viewBox="0 0 1000 1000"
+ version="1.1"
+ xml:space="preserve"
+ id="SVGRoot"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><defs
+ id="defs9" />
+
+<style
+ type="text/css"
+ id="style1">
+g.prefab path {
+ vector-effect:non-scaling-stroke;
+ -inkscape-stroke:hairline;
+ fill: none;
+ fill-opacity: 1;
+ stroke-opacity: 1;
+ stroke: #00349c;
+}
+</style>
+
+<path
+ id="rect10"
+ style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
+ d="M 300,250 H 450 V 750 H 300 Z m 250,0 H 700 V 750 H 550 Z" /><path
+ style="display:none;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
+ id="path10"
+ d="M 800,500 574.99999,629.90381 350,759.80762 l 0,-259.80763 0,-259.80761 225.00001,129.90381 z"
+ transform="translate(-50)" /></svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="1000"
+ height="1000"
+ viewBox="0 0 1000 1000"
+ version="1.1"
+ xml:space="preserve"
+ id="SVGRoot"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><defs
+ id="defs9" />
+
+<style
+ type="text/css"
+ id="style1">
+g.prefab path {
+ vector-effect:non-scaling-stroke;
+ -inkscape-stroke:hairline;
+ fill: none;
+ fill-opacity: 1;
+ stroke-opacity: 1;
+ stroke: #00349c;
+}
+</style>
+
+<path
+ id="rect10"
+ style="display:none;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
+ d="M 300,250 H 450 V 750 H 300 Z m 250,0 H 700 V 750 H 550 Z" /><path
+ style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-dasharray:none;stroke-opacity:1"
+ id="path10"
+ d="M 800,500 574.99999,629.90381 350,759.80762 l 0,-259.80763 0,-259.80761 225.00001,129.90381 z"
+ transform="translate(-50)" /></svg>
--- /dev/null
+<html>
+ <head>
+ <style>
+
+body {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: 0;
+ overflow: hidden;
+ color: var(--background)
+}
+img {
+ height: 100vh;
+}
+audio {
+ margin: 4px;
+ width: calc(100% - 8px);
+ height: 20px;
+}
+#playerMain {
+ display: inline-block;
+ vertical-align: top;
+ width: calc((100vw - 100vh) * 0.5);
+ height: 100vh;
+
+ background-color: var(--primary);
+}
+#playerInfo {
+ display: inline-block;
+ vertical-align: top;
+ width: calc(((100vw - 100vh) * 0.5) - 1px);
+ height: 100vh;
+ overflow: scroll;
+
+ background-color: var(--secondary);
+}
+#stationName {
+ position: absolute;
+ bottom: 0;
+ left: 100vh;
+ margin: 0;
+ color: var(--off-primary);
+ font-size: 2em;
+ line-height: 1em;
+}
+#playerPlaying {
+ width: calc(100% - 16px);
+ height: 20px;
+ margin: 4px;
+ padding: 4px;
+ color: var(--primary);
+ background-color: var(--dark);
+}
+#pauseIcon, #playIcon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vh;
+ height: 100vh;
+}
+#allIcon {
+ opacity: 0;
+ transition: opacity 0.5s;
+}
+#allIcon:hover {
+ opacity: 1;
+}
+
+@media only screen and (max-width : 700px) {
+ #playerMain {
+ width: calc((100vw - 100vh) - 1px);
+ }
+ #playerInfo {
+ display: none;
+ }
+}
+
+/* Remove default apperance */
+input[type="range"] {
+ margin: 4px;
+ margin-bottom: 0;
+ width: calc(100% - 8px);
+ height: 10px;
+ -webkit-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ outline: none;
+ /* overflow: hidden; remove this line*/
+ background: var(--background);
+}
+
+/* Thumb: webkit */
+input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 10px;
+ width: 10px;
+ background-color: var(--off-primary);
+ border-radius: 0;
+ border: none;
+ transition: .2s ease-in-out;
+}
+
+/* Thumb: Firefox */
+input[type="range"]::-moz-range-thumb {
+ height: 10px;
+ width: 30px;
+ background-color: var(--off-primary);
+ border-radius: 0;
+ border: none;
+ transition: .2s ease-in-out;
+}
+
+/* Hover, active & focus Thumb: Webkit */
+
+/*input[type="range"]::-webkit-slider-thumb:hover {
+ box-shadow: 0 0 0 10px rgba(255,85,0, .1)
+}
+input[type="range"]:active::-webkit-slider-thumb {
+ box-shadow: 0 0 0 13px rgba(255,85,0, .2)
+}
+input[type="range"]:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 13px rgba(255,85,0, .2)
+}*/
+
+/* Hover, active & focus Thumb: Firfox */
+
+/*input[type="range"]::-moz-range-thumb:hover {
+ box-shadow: 0 0 0 10px rgba(255,85,0, .1)
+}
+input[type="range"]:active::-moz-range-thumb {
+ box-shadow: 0 0 0 13px rgba(255,85,0, .2)
+}
+input[type="range"]:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 13px rgba(255,85,0, .2)
+}*/
+ </style>
+ <link rel="stylesheet" href="/static/style.css">
+ <script>
+
+const player = new Audio("https://stream.ozva.co.uk:8443/critters");
+var playing = false;
+
+function getInfo () {
+ fetch("/api/0")
+ .then(data => data.text())
+ .then(data => JSON.parse(data))
+ .then(data => {
+ document.getElementById("playerCover").src = data.cover_path;
+ document.getElementById("playerInfo").innerHTML = data.info;
+ document.getElementById("playerPlaying").innerHTML = `
+ <em>Now playing:</em> <a href="${data.link}">${data.title}</a> - ${data.artist} <em>(${data.year})</em>
+ `;
+ })
+}
+
+function setVolume () {
+ player.volume = document.getElementById("volumeControl").value;
+}
+
+function toggleAudio () {
+ if (playing) {
+ player.pause();
+ player.load();
+ playing = false;
+ document.getElementById("playIcon").style = "";
+ document.getElementById("pauseIcon").style = "display: none;";
+ } else {
+ player.play();
+ playing = true;
+ document.getElementById("playIcon").style = "display: none;";
+ document.getElementById("pauseIcon").style = "";
+ }
+}
+
+setInterval(setVolume, 100);
+setInterval(getInfo, 4000);
+ </script>
+ </head>
+ <body onload="getInfo();document.getElementById('volumeControl').style = '';">
+ <img id="playerCover" src="/api/cover/0" /><div id="playerMain">
+ <input id="volumeControl" style="display: none;" type="range" min="0" max="1" step="0.01"/>
+ <noscript>
+ <audio src="https://stream.ozva.co.uk:8443/critters" controls></audio><br>
+ </noscript>
+ <p id="playerPlaying">Currently playing track info available with the JS player</p>
+ </div><div id="playerInfo">
+ </div>
+ <h1 id="stationName"><em>Tree Critters Radio</em></h1>
+ <div id="allIcon">
+ <img onclick="toggleAudio();" id="playIcon" style="" src="/static/play.svg" />
+ <img onclick="toggleAudio();" id="pauseIcon" style="display: none;" src="/static/pause.svg" />
+ </div>
+ </body>
+</html>
--- /dev/null
+:root {
+ --background: #111111;
+ --dark: #151515;
+ --primary: #70b783;
+ --off-primary: #4d9961;
+ --secondary: #c6ce73;
+ --tertiary: #2b5536;
+
+ font-family: sans-serif;
+ color: var(--primary);
+ background-color: var(--background);
+}
+
+a:link {
+ color: var(--secondary);
+}
+a:visited {
+ color: var(--secondary);
+}
+a:hover {
+ color: var(--primary);
+}
+a:active {
+ color: white;
+}
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tree Critters</title>
+ <script src="static/admin.js"></script>
+ <style>
+
+input[type="submit"], input[type="button"] {
+ color: var(--background);
+ background-color: var(--secondary);
+ border: none;
+}
+input[type="submit"]:hover, input[type="button"]:hover {
+ background-color: var(--primary);
+}
+
+#trackList {
+ display: inline-block;
+ width: 78%;
+ margin: 1%;
+ vertical-align: top;
+}
+#editPannel {
+ display: inline-block;
+ width: 18%;
+ margin: 1%;
+ vertical-align: top;
+}
+
+table, input[type="text"] {
+ width: 100%;
+ padding: 0;
+ border-collapse: collapse;
+}
+th {
+ text-align: left;
+}
+
+#thumbnail {
+ width: 100%;
+}
+
+.smallIcon {
+ width: 1em;
+ height: 1em;
+}
+.mainForm {
+ color: var(--background);
+}
+
+ </style>
+ <link rel="stylesheet" href="/static/style.css">
+ </head>
+ <body>
+ <h1>Tree Critters Admin pannel</h1>
+ <p>Signed in as <b>{{user}}</b></p>
+ <input type="button" value="Refresh" onclick="getTrackList();" /><br>
+ <div id="editPannel">
+ <img id="thumbnail" src="/api/cover/0" />
+ <span id="cover_path" value=""></span>
+ <input type="button" value="New track" onclick="newTrack();" />
+ <form method="POST" enctype=multipart/form-data>
+ <table>
+ <tr>
+ <td><label for="id">Track ID</label></td>
+ <td><input type="text" id="id" class="mainForm" name="id" value="New track" readonly></input></td>
+ </tr>
+ <tr>
+ <td><label for="title">Title</label></td>
+ <td><input type="text" id="title" class="mainForm" name="title"></input></td>
+ </tr>
+ <tr>
+ <td><label for="album">Album</label></td>
+ <td><input type="text" id="album" class="mainForm" name="album"></input></td>
+ </tr>
+ <tr>
+ <td><label for="artist">Artist</label></td>
+ <td><input type="text" id="artist" class="mainForm" name="artist"></input></td>
+ </tr>
+ <tr>
+ <td><label for="year">Year</label></td>
+ <td><input type="text" id="year" class="mainForm" name="year"></input></td>
+ </tr>
+ <tr>
+ <td><label for="link">URL</label></td>
+ <td><input type="text" id="link" class="mainForm" name="link"></input></td>
+ </tr>
+ <tr>
+ <td><label for="info">Track info</label></td>
+ <td><input type="text" id="info" class="mainForm" name="info"></input></td>
+ </tr>
+ <tr>
+ <td><label for="category">Category</label></td>
+ <td><input type="text" id="category" class="mainForm" name="category"></input></td>
+ </tr>
+ <tr>
+ <td><label for="playing">Playing</label></td>
+ <td><input type="checkbox" id="playing" class="mainForm" name="playing" value="true"></input></td>
+ </tr>
+ <tr>
+ <td><label for="intime">In-time</label></td>
+ <td><input type="number" id="intime" class="mainForm" name="intime"></input></td>
+ </tr>
+ <tr>
+ <td><label for="outtime">Out-time</label></td>
+ <td><input type="number" id="outtime" class="mainForm" name="outtime"></input></td>
+ </tr>
+
+
+ <tr>
+ <td><label for="audio">Track</label></td>
+ <td><input type="file" id="audio" name="audio"></td>
+ </tr>
+ <tr>
+ <td><label for="front">Front</label></td>
+ <td><input type="file" id="front" name="front"></td>
+ </tr>
+ <tr>
+ <td><label for="back">Back</label></td>
+ <td><input type="file" id="back" name="back"></td>
+ </tr>
+ <tr>
+ <td><label for="cover">Cover</label></td>
+ <td><input type="file" id="cover" name="cover"></td>
+ </tr>
+
+
+ </table>
+ <input type="submit" value="Save data" />
+ </form>
+ </div><div id="trackList"><table id="trackListFull"></table></div>
+ </body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Tree Critters</title>
+ <link rel="stylesheet" href="static/style.css">
+ </head>
+ <body>
+ <h1>Tree Critters Radio</h1>
+ <h2>Player:</h2>
+ <iframe style="border: none; width: 700px; height: 100px;" src="/static/player.html"></iframe>
+ <p><a href="/admin">Control pannel</a></p>
+ </body>
+</html>