Compare commits
28 Commits
781aebc6ad
...
f51b523cd9
Author | SHA1 | Date |
---|---|---|
scoobybejesus | f51b523cd9 | 8 months ago |
dsc | bbb9c7b0c6 | 8 months ago |
dsc | 0bd046cf77 | 8 months ago |
Ricky 'fluffybrony' Spaghettio | 590a6ffec2 | 8 months ago |
dsc | a0440060b4 | 8 months ago |
dsc | cabef9ffe2 | 8 months ago |
dsc | 1f04b1e9d5 | 8 months ago |
dsc | 9cff22f6ac | 8 months ago |
dsc | ba37f74599 | 8 months ago |
dsc | c59e2edf8e | 8 months ago |
dsc | 238022c052 | 8 months ago |
dsc | 311e3279bf | 8 months ago |
dsc | 4a6d6025d7 | 8 months ago |
dsc | 62c04edc09 | 8 months ago |
dsc | abb67ada08 | 8 months ago |
dsc | 0bf9a07cf0 | 8 months ago |
dsc | 5d8caf4d24 | 8 months ago |
dsc | 1cf5b0e79a | 8 months ago |
dsc | 22441c932a | 8 months ago |
dsc | 9a23d7e71f | 8 months ago |
dsc | 48b1189c3e | 10 months ago |
dsc | 077ce3f3b7 | 2 years ago |
dsc | 67ec5a5999 | 2 years ago |
dsc | 8aac79cd5f | 2 years ago |
scoobybejesus | 9a66e1b853 | 2 years ago |
scoobybejesus | eb74dc4ae5 | 2 years ago |
scoobybejesus | 25b46feac7 | 2 years ago |
dsc | 337831ea1d | 2 years ago |
@ -0,0 +1,175 @@
|
||||
#!/usr/bin/liquidsoap
|
||||
set("log.stdout", true)
|
||||
set("log.file",false)
|
||||
#%include "cross.liq"
|
||||
|
||||
# Allow requests from Telnet (Liquidsoap Requester)
|
||||
set("server.telnet", true)
|
||||
set("server.telnet.bind_addr", "127.0.0.1")
|
||||
set("server.telnet.port", 7555)
|
||||
set("server.telnet.reverse_dns", false)
|
||||
|
||||
|
||||
pmain = playlist(
|
||||
id="playlist",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/ircradio/data/music"
|
||||
)
|
||||
|
||||
|
||||
# ==== ANJUNADEEP
|
||||
panjunadeep = playlist(
|
||||
id="panjunadeep",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/anjunadeep/"
|
||||
)
|
||||
|
||||
# ==== BERLIN
|
||||
pberlin = playlist(
|
||||
id="berlin",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/berlin/"
|
||||
)
|
||||
|
||||
# ==== BREAKBEAT
|
||||
pbreaks = playlist(
|
||||
id="breaks",
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/breakbeat/"
|
||||
)
|
||||
|
||||
# ==== DNB
|
||||
pdnb = playlist(
|
||||
id="dnb",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/dnb/"
|
||||
)
|
||||
|
||||
# ==== RAVES
|
||||
praves = playlist(
|
||||
id="raves",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/raves/"
|
||||
)
|
||||
|
||||
# ==== TRANCE
|
||||
ptrance = playlist(
|
||||
id="trance",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/trance/"
|
||||
)
|
||||
|
||||
# ==== WEED
|
||||
pweed = playlist(
|
||||
id="weed",
|
||||
timeout=90.0,
|
||||
mode="random",
|
||||
reload=300,
|
||||
reload_mode="seconds",
|
||||
mime_type="audio/ogg",
|
||||
"/home/radio/mixes/weed/"
|
||||
)
|
||||
|
||||
req_pmain = request.queue(id="pmain")
|
||||
req_panjunadeep = request.queue(id="panjunadeep")
|
||||
req_pberlin = request.queue(id="pberlin")
|
||||
req_pbreaks = request.queue(id="pbreaks")
|
||||
req_pdnb = request.queue(id="pdnb")
|
||||
req_praves = request.queue(id="praves")
|
||||
req_ptrance = request.queue(id="ptrance")
|
||||
req_pweed = request.queue(id="pweed")
|
||||
|
||||
pmain = fallback(id="switcher",track_sensitive = true, [req_pmain, pmain, blank(duration=5.)])
|
||||
panjunadeep = fallback(id="switcher",track_sensitive = true, [req_panjunadeep, panjunadeep, blank(duration=5.)])
|
||||
pberlin = fallback(id="switcher",track_sensitive = true, [req_pberlin, pberlin, blank(duration=5.)])
|
||||
pbreaks = fallback(id="switcher",track_sensitive = true, [req_pbreaks, pbreaks, blank(duration=5.)])
|
||||
pdnb = fallback(id="switcher",track_sensitive = true, [req_pdnb, pdnb, blank(duration=5.)])
|
||||
praves = fallback(id="switcher",track_sensitive = true, [req_praves, praves, blank(duration=5.)])
|
||||
ptrance = fallback(id="switcher",track_sensitive = true, [req_ptrance, ptrance, blank(duration=5.)])
|
||||
pweed = fallback(id="switcher",track_sensitive = true, [req_pweed, pweed, blank(duration=5.)])
|
||||
|
||||
# iTunes-style (so-called "dumb" - but good enough) crossfading
|
||||
pmain_crossed = crossfade(pmain)
|
||||
pmain_crossed = mksafe(pmain_crossed)
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio",
|
||||
password = "lel", mount = "wow.ogg",
|
||||
pmain_crossed)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Anjunadeep",
|
||||
password = "lel", mount = "anjunadeep.ogg",
|
||||
panjunadeep)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Berlin",
|
||||
password = "lel", mount = "berlin.ogg",
|
||||
pberlin)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Breakbeat",
|
||||
password = "lel", mount = "breaks.ogg",
|
||||
pbreaks)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Dnb",
|
||||
password = "lel", mount = "dnb.ogg",
|
||||
pdnb)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Raves",
|
||||
password = "lel", mount = "raves.ogg",
|
||||
praves)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Trance",
|
||||
password = "lel", mount = "trance.ogg",
|
||||
ptrance)
|
||||
|
||||
|
||||
output.icecast(%vorbis.cbr(samplerate=48000, channels=2, bitrate=164),
|
||||
host = "10.7.0.3", port = 24100,
|
||||
icy_metadata="true", description="WOW!Radio | Weed",
|
||||
password = "lel", mount = "weed.ogg",
|
||||
pweed)
|
@ -1,4 +1,8 @@
|
||||
from ircradio.utils import liquidsoap_check_symlink
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import settings
|
||||
|
||||
liquidsoap_check_symlink()
|
||||
with open(os.path.join(settings.cwd, 'data', 'agents.txt'), 'r') as f:
|
||||
user_agents: Optional[List[str]] = [
|
||||
l.strip() for l in f.readlines() if l.strip()]
|
||||
|
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 224 KiB |
After Width: | Height: | Size: 145 KiB |
@ -0,0 +1,43 @@
|
||||
.hero {max-width:700px;}
|
||||
a{color: #82b2e5;}
|
||||
body {background-color: inherit !important; background: linear-gradient(45deg, #07070a, #001929);}
|
||||
button, small.footer { text-align: left !important; }
|
||||
button[data-playing="true"] { color: white; }
|
||||
.container {max-width:96%;}
|
||||
.card {border: 1px solid rgba(250,250,250,.1) !important;}
|
||||
.card-body {background-color: #151515;}
|
||||
.card-header{background-color: rgb(25, 25, 25) !important; text-align: center;}
|
||||
.text-muted { color: #dedede !important;}
|
||||
.btn-outline-primary {
|
||||
color: #5586b7;
|
||||
border-color: #527ca8;
|
||||
}
|
||||
@keyframes gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
.btn-active {
|
||||
color:white;
|
||||
background: linear-gradient(45deg, rgba(255, 42, 212, 1) 0%, rgba(255, 204, 0, 1) 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 3s ease infinite;
|
||||
}
|
||||
.bootstrap-table .search {
|
||||
float: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.card-header {padding: .5rem 1rem;}
|
||||
.card-header .font-weight-normal {font-size: 1.2rem;}
|
||||
.card-img-top {
|
||||
width: 100%;
|
||||
max-height: 170px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.card-footer {
|
||||
border:none !important;
|
||||
background-color: #151515 !important;
|
||||
}
|
||||
@media screen and (min-width:1100px){
|
||||
.container {max-width:1200px;}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
function ws_connect(ws_url, onData) {
|
||||
console.log('connecting');
|
||||
var ws = new WebSocket(ws_url);
|
||||
ws.onopen = function() {
|
||||
// nothing
|
||||
};
|
||||
|
||||
ws.onmessage = function(e) {
|
||||
console.log('Message:', e.data);
|
||||
onData(e.data);
|
||||
};
|
||||
|
||||
ws.onclose = function(e) {
|
||||
console.log('Socket is closed. Reconnect will be attempted in 2 seconds.', e.reason);
|
||||
setTimeout(function() {
|
||||
ws_connect(ws_url, onData);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
ws.onerror = function(err) {
|
||||
console.error('Socket encountered error: ', err.message, 'Closing socket');
|
||||
ws.close();
|
||||
};
|
||||
}
|
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 129 KiB |
After Width: | Height: | Size: 200 KiB |
@ -1,79 +0,0 @@
|
||||
// tracks input in 'search' field (id=general)
|
||||
let input_so_far = "";
|
||||
|
||||
// cached song list and cached queries
|
||||
var queries = [];
|
||||
var songs = new Map([]);
|
||||
|
||||
// track async fetch and processing
|
||||
var returned = false;
|
||||
|
||||
$("#general").keyup( function() {
|
||||
|
||||
input_so_far = document.getElementsByName("general")[0].value;
|
||||
|
||||
if (input_so_far.length < 3) {
|
||||
$("#table tr").remove();
|
||||
return
|
||||
};
|
||||
|
||||
if (!queries.includes(input_so_far.toLowerCase() ) ) {
|
||||
queries.push(input_so_far.toLowerCase() );
|
||||
returned = false;
|
||||
|
||||
const sanitized_input = encodeURIComponent( input_so_far );
|
||||
const url = 'https://' + document.domain + ':' + location.port + '/search?name=' + sanitized_input + '&limit=15&offset=0'
|
||||
|
||||
const LoadData = async () => {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
console.log("Status code 200 or similar: " + res.ok);
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
};
|
||||
|
||||
LoadData().then(newSongsJson => {
|
||||
newSongsJson.forEach( (new_song) => {
|
||||
let already_have = false;
|
||||
songs.forEach( (_v, key) => {
|
||||
if (new_song.id == key) { already_have = true; return; };
|
||||
})
|
||||
if (!already_have) { songs.set(new_song.utube_id, new_song) }
|
||||
})
|
||||
}).then( () => { returned = true } );
|
||||
|
||||
};
|
||||
|
||||
function renderTable () {
|
||||
|
||||
if (returned) {
|
||||
|
||||
$("#table tr").remove();
|
||||
|
||||
var filtered = new Map(
|
||||
[...songs]
|
||||
.filter(([k, v]) =>
|
||||
( v.title.toLowerCase().includes( input_so_far.toLowerCase() ) ) ||
|
||||
( v.added_by.toLowerCase().includes( input_so_far.toLowerCase() ) ) )
|
||||
);
|
||||
|
||||
filtered.forEach( (song) => {
|
||||
let added = song.added_by;
|
||||
let added_link = '<a href="/library?name=' + added + '" target="_blank" rel="noopener noreferrer">' + added + '</a>';
|
||||
let title = song.title;
|
||||
let id = song.utube_id;
|
||||
let id_link = '<a href="https://www.youtube.com/watch?v=' + id + '" target="_blank" rel="noopener noreferrer">' + id + '</a>';
|
||||
$('#table tbody').append('<tr><td>'+id_link+'</td><td>'+added_link+'</td><td>'+title+'</td></tr>')
|
||||
})
|
||||
|
||||
} else {
|
||||
setTimeout(renderTable, 30); // try again in 30 milliseconds
|
||||
}
|
||||
};
|
||||
|
||||
renderTable();
|
||||
|
||||
});
|
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,188 @@
|
||||
import re, os, json, logging
|
||||
from collections import OrderedDict
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import mutagen
|
||||
import aiofiles
|
||||
from aiocache import cached, Cache
|
||||
from aiocache.serializers import PickleSerializer
|
||||
|
||||
import settings
|
||||
from ircradio.models import Song
|
||||
|
||||
|
||||
@dataclass
|
||||
class SongDataclass:
|
||||
title: str
|
||||
karma: int
|
||||
utube_id: str
|
||||
added_by: str
|
||||
image: Optional[str]
|
||||
duration: Optional[int]
|
||||
progress: Optional[int] # pct
|
||||
progress_str: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Station:
|
||||
id: str
|
||||
music_dir: str # /full/path/to/music/
|
||||
mount_point: str # wow.ogg
|
||||
request_id: str # pberlin (for telnet requests)
|
||||
title: str # for webif
|
||||
description: str # for webif
|
||||
image: str # for webif
|
||||
listeners: int = 0
|
||||
|
||||
song: Optional[SongDataclass] = None
|
||||
|
||||
async def skip(self) -> bool:
|
||||
from ircradio.radio import Radio
|
||||
try:
|
||||
await Radio.command(self.telnet_cmd_skip)
|
||||
return True
|
||||
except Exception as ex:
|
||||
return False
|
||||
|
||||
async def np(self) -> Optional[Song]:
|
||||
history = await self.history()
|
||||
if history:
|
||||
return history[-1]
|
||||
|
||||
@cached(ttl=4, cache=Cache.MEMORY,
|
||||
key_builder=lambda *args, **kw: f"history_station_{args[1].id}",
|
||||
serializer=PickleSerializer())
|
||||
async def history(self) -> List[Song]:
|
||||
from ircradio.radio import Radio
|
||||
# 1. ask liquidsoap for history
|
||||
# 2. check database (Song.from_filepath)
|
||||
# 3. check direct file exif (Song.from_filepath)
|
||||
# 4. check .ogg.json metadata file (Song.from_filepath)
|
||||
# 5. verify the above by comparing icecast metadata
|
||||
|
||||
liq_meta = await Radio.command(self.telnet_cmd_metadata)
|
||||
liq_meta = liq_meta.decode(errors="ignore")
|
||||
liq_filenames = re.findall(r"filename=\"(.*)\"", liq_meta)
|
||||
liq_filenames = [fn for fn in liq_filenames if os.path.exists(fn)]
|
||||
|
||||
liq_remaining = await Radio.command(self.telnet_cmd_remaining)
|
||||
liq_remaining = liq_remaining.decode(errors="ignore")
|
||||
|
||||
remaining = None
|
||||
if re.match('\d+.\d+', liq_remaining):
|
||||
remaining = int(liq_remaining.split('.')[0])
|
||||
|
||||
songs = []
|
||||
for liq_fn in liq_filenames:
|
||||
try:
|
||||
song = await Song.from_filepath(liq_fn)
|
||||
songs.append(song)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
|
||||
if not songs:
|
||||
return []
|
||||
|
||||
# icecast compare, silliness ahead
|
||||
meta: dict = await Radio.icecast_metadata(self)
|
||||
if meta:
|
||||
meta_title = meta.get('title')
|
||||
if meta_title and meta_title.lower() in [' ', 'unknown', 'error', 'empty', 'bleepbloopblurp']:
|
||||
meta_title = None
|
||||
|
||||
if meta_title and len(songs) > 1 and songs:
|
||||
title_a = songs[-1].title_for_real_nocap.lower()
|
||||
title_b = meta_title.lower()
|
||||
|
||||
if title_a not in title_b and title_b not in title_a:
|
||||
# song detection and actual icecast metadata differ
|
||||
logging.error(f"song detection and icecast metadata differ:\n{meta_title}\n{songs[-1].title}")
|
||||
songs[-1].title = meta_title
|
||||
|
||||
if remaining:
|
||||
songs[-1].remaining = remaining
|
||||
|
||||
return songs
|
||||
|
||||
async def queue_get(self) -> List[Song]:
|
||||
from ircradio.radio import Radio
|
||||
|
||||
queues = await Radio.command(self.telnet_cmd_queue)
|
||||
queue_ids = re.findall(b"\d+", queues)
|
||||
if not queue_ids:
|
||||
return []
|
||||
|
||||
queue_ids: List[int] = list(reversed(list(map(int, queue_ids)))) # yolo
|
||||
|
||||
songs = []
|
||||
for request_id in queue_ids:
|
||||
liq_meta = await Radio.command(f"request.metadata {request_id}")
|
||||
liq_meta = liq_meta.decode(errors='none')
|
||||
liq_fn = re.findall(r"filename=\"(.*)\"", liq_meta)
|
||||
if not liq_fn:
|
||||
continue
|
||||
liq_fn = liq_fn[0]
|
||||
|
||||
try:
|
||||
song = await Song.from_filepath(liq_fn)
|
||||
songs.append(song)
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
|
||||
# remove the now playing song from the queue
|
||||
now_playing = await self.np()
|
||||
if songs and now_playing:
|
||||
if songs[0].filepath == now_playing.filepath:
|
||||
songs = songs[1:]
|
||||
return songs
|
||||
|
||||
async def queue_push(self, filepath: str) -> bool:
|
||||
from ircradio.radio import Radio
|
||||
from ircradio.factory import app
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
logging.error(f"file does not exist: {filepath}")
|
||||
return False
|
||||
|
||||
current_queue = await self.queue_get()
|
||||
if filepath in [c.filepath for c in current_queue]:
|
||||
logging.error(f"already added to queue: {song.filepath}")
|
||||
return False
|
||||
|
||||
try:
|
||||
await Radio.command(f"{self.telnet_cmd_push} {filepath}")
|
||||
except Exception as ex:
|
||||
logging.error(f"failed to push, idunno; {ex}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def get_listeners(self) -> int:
|
||||
from ircradio.radio import Radio
|
||||
meta: dict = await Radio.icecast_metadata(self)
|
||||
return meta.get('listeners', 0)
|
||||
|
||||
@property
|
||||
def telnet_cmd_push(self): # push into queue
|
||||
return f"{self.request_id}.push"
|
||||
|
||||
@property
|
||||
def telnet_cmd_queue(self): # view queue_ids
|
||||
return f"{self.request_id}.queue"
|
||||
|
||||
@property
|
||||
def telnet_cmd_metadata(self):
|
||||
return f"{self.mount_point}.metadata"
|
||||
|
||||
@property
|
||||
def telnet_cmd_remaining(self):
|
||||
return f"{self.mount_point}.remaining"
|
||||
|
||||
@property
|
||||
def telnet_cmd_skip(self):
|
||||
return f"{self.mount_point}.skip"
|
||||
|
||||
@property
|
||||
def stream_url(self):
|
||||
return f"http://{settings.icecast2_hostname}/{self.mount_point}"
|
@ -1,17 +0,0 @@
|
||||
[Unit]
|
||||
Description={{ description }}
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User={{ user }}
|
||||
Group={{ group }}
|
||||
Environment="{{ env }}"
|
||||
StateDirectory={{ name | lower }}
|
||||
LogsDirectory={{ name | lower }}
|
||||
Type=simple
|
||||
ExecStart={{ path_executable }} {{ args_executable }}
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,75 +0,0 @@
|
||||
# Crossfade between tracks,
|
||||
# taking the respective volume levels
|
||||
# into account in the choice of the
|
||||
# transition.
|
||||
# @category Source / Track Processing
|
||||
# @param ~start_next Crossing duration, if any.
|
||||
# @param ~fade_in Fade-in duration, if any.
|
||||
# @param ~fade_out Fade-out duration, if any.
|
||||
# @param ~width Width of the volume analysis window.
|
||||
# @param ~conservative Always prepare for
|
||||
# a premature end-of-track.
|
||||
# @param s The input source.
|
||||
def smart_crossfade (~start_next=5.,~fade_in=3.,
|
||||
~fade_out=3., ~width=2.,
|
||||
~conservative=false,s)
|
||||
high = -20.
|
||||
medium = -32.
|
||||
margin = 4.
|
||||
fade.out = fade.out(type="sin",duration=fade_out)
|
||||
fade.in = fade.in(type="sin",duration=fade_in)
|
||||
add = fun (a,b) -> add(normalize=false,[b,a])
|
||||
log = log(label="smart_crossfade")
|
||||
def transition(a,b,ma,mb,sa,sb)
|
||||
list.iter(fun(x)->
|
||||
log(level=4,"Before: #{x}"),ma)
|
||||
list.iter(fun(x)->
|
||||
log(level=4,"After : #{x}"),mb)
|
||||
if
|
||||
# If A and B and not too loud and close,
|
||||
# fully cross-fade them.
|
||||
a <= medium and
|
||||
b <= medium and
|
||||
abs(a - b) <= margin
|
||||
then
|
||||
log("Transition: crossed, fade-in, fade-out.")
|
||||
add(fade.out(sa),fade.in(sb))
|
||||
elsif
|
||||
# If B is significantly louder than A,
|
||||
# only fade-out A.
|
||||
# We don't want to fade almost silent things,
|
||||
# ask for >medium.
|
||||
b >= a + margin and a >= medium and b <= high
|
||||
then
|
||||
log("Transition: crossed, fade-out.")
|
||||
add(fade.out(sa),sb)
|
||||
elsif
|
||||
# Do not fade if it's already very low.
|
||||
b >= a + margin and a <= medium and b <= high
|
||||
then
|
||||
log("Transition: crossed, no fade-out.")
|
||||
add(sa,sb)
|
||||
elsif
|
||||
# Opposite as the previous one.
|
||||
a >= b + margin and b >= medium and a <= high
|
||||
then
|
||||
log("Transition: crossed, fade-in.")
|
||||
add(sa,fade.in(sb))
|
||||
# What to do with a loud end and
|
||||
# a quiet beginning ?
|
||||
# A good idea is to use a jingle to separate
|
||||
# the two tracks, but that's another story.
|
||||
else
|
||||
# Otherwise, A and B are just too loud
|
||||
# to overlap nicely, or the difference
|
||||
# between them is too large and
|
||||
# overlapping would completely mask one
|
||||
# of them.
|
||||
log("No transition: just sequencing.")
|
||||
sequence([sa, sb])
|
||||
end
|
||||
end
|
||||
cross(width=width, duration=start_next,
|
||||
conservative=conservative,
|
||||
transition,s)
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
<footer class="pt-4 my-md-5 pt-md-5 border-top">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md">
|
||||
<img class="mb-2" src="{{ url_for('static', filename='wow.png') }}" alt="" width="24" height="24">
|
||||
<small class="d-block mb-3 text-muted">IRC!Radio</small>
|
||||
</div>
|
||||
<div class="col-4 col-md">
|
||||
<h5>Menu</h5>
|
||||
<ul class="list-unstyled text-small">
|
||||
<li><a class="text-muted" href=" {{ url_for('login') }} ">Login</a></li>
|
||||
<li><a class="text-muted" href=" {{ url_for('history') }} ">History</a></li>
|
||||
<li><a class="text-muted" href=" {{ url_for('user_library') }} ">User Library</a></li>
|
||||
<li><a class="text-muted" href=" {{ url_for('request_song') }} ">Request</a></li>
|
||||
<li><a class="text-muted" href="https://git.wownero.com/dsc/ircradio">Source</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-4 col-md">
|
||||
</div>
|
||||
|
||||
<div class="col-4 col-md">
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<h5>History</h5>
|
||||
<pre style="color:white;font-family:monospace;font-size:14px;">{% for s in songs %}<a target="_blank" href="https://www.youtube.com/watch?v={{s.utube_id}}">{{s.utube_id}}</a> {{s.title}}
|
||||
{% endfor %}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
@ -1,53 +0,0 @@
|
||||
<icecast>
|
||||
<location>Somewhere</location>
|
||||
<admin>my@email.tld</admin>
|
||||
|
||||
<limits>
|
||||
<clients>32</clients>
|
||||
<sources>2</sources>
|
||||
<queue-size>524288</queue-size>
|
||||
<client-timeout>30</client-timeout>
|
||||
<header-timeout>15</header-timeout>
|
||||
<source-timeout>10</source-timeout>
|
||||
<burst-on-connect>0</burst-on-connect>
|
||||
<burst-size>65535</burst-size>
|
||||
</limits>
|
||||
|
||||
<authentication>
|
||||
<source-password>{{ source_password }}</source-password>
|
||||
<relay-password>{{ relay_password }}</relay-password> <!-- for livestreams -->
|
||||
<admin-user>admin</admin-user>
|
||||
<admin-password>{{ admin_password }}</admin-password>
|
||||
</authentication>
|
||||
|
||||
<hostname>{{ hostname }}</hostname>
|
||||
|
||||
<listen-socket>
|
||||
<bind-address>{{ icecast2_bind_host }}</bind-address>
|
||||
<port>{{ icecast2_bind_port }}</port>
|
||||
</listen-socket>
|
||||
|
||||
<http-headers>
|
||||
<header name="Access-Control-Allow-Origin" value="*" />
|
||||
</http-headers>
|
||||
|
||||
<fileserve>1</fileserve>
|
||||
|
||||
<paths>
|
||||
<basedir>/usr/share/icecast2</basedir>
|
||||
<logdir>{{ log_dir }}</logdir>
|
||||
<webroot>/usr/share/icecast2/web</webroot>
|
||||
<adminroot>/usr/share/icecast2/admin</adminroot>
|
||||
</paths>
|
||||
|
||||
<logging>
|
||||
<accesslog>icecast2_access.log</accesslog>
|
||||
<errorlog>icecast2_error.log</errorlog>
|
||||
<loglevel>3</loglevel> <!-- 4 Debug, 3 Info, 2 Warn, 1 Error -->
|
||||
<logsize>10000</logsize> <!-- Max size of a logfile -->
|
||||
</logging>
|
||||
|
||||
<security>
|
||||
<chroot>0</chroot>
|
||||
</security>
|
||||
</icecast>
|
@ -1,93 +1,245 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% set radio_default = settings.radio_stations['wow'] %}
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- Post Content Column -->
|
||||
<div class="col-lg-12">
|
||||
<!-- Title -->
|
||||
<h1 class="mt-4" style="margin-bottom: 2rem;">
|
||||
IRC!Radio
|
||||
</h1>
|
||||
<p>Enjoy the music :)</p>
|
||||
<hr>
|
||||
<audio controls src="/{{ settings.icecast2_mount }}">Your browser does not support the<code>audio</code> element.</audio>
|
||||
<p> </p>
|
||||
<h5>Now playing: </h5>
|
||||
<div id="now_playing">Nothing here yet</div>
|
||||
<hr>
|
||||
|
||||
<h4>Command list:</h4>
|
||||
<pre style="font-size:12px;">!np - current song
|
||||
!tune - upvote song
|
||||
!boo - downvote song
|
||||
!request - search and queue a song by title or YouTube id
|
||||
!dj+ - add a YouTube ID to the radiostream
|
||||
!dj- - remove a YouTube ID
|
||||
!ban+ - ban a YouTube ID and/or nickname
|
||||
!ban- - unban a YouTube ID and/or nickname
|
||||
!skip - skips current song
|
||||
!listeners - show current amount of listeners
|
||||
!queue - show queued up music
|
||||
!queue_user - queue a random song by user
|
||||
!search - search for a title
|
||||
!stats - stats
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<h4>History</h4>
|
||||
<a href="/history.txt">history.txt</a>
|
||||
{% for rs in radio_stations %}
|
||||
<div class="col-md-4 col-sm-6 col-xl-3 d-flex">
|
||||
<div data-radio="{{ rs.id }}" class="card box-shadow mb-4 flex-fill">
|
||||
<div class="card-header">
|
||||
<h5 class="my-0 font-weight-normal">{{ rs.title }}</h5>
|
||||
</div>
|
||||
<img class="img_header card-img-top" src="{{ url_for('static', filename=rs.image) }}" alt="">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="title_str card-title pricing-card-title text-muted">|</h5>
|
||||
<ul class="list-unstyled mt-3 mb-4">
|
||||
<li class="d-none listeners_str">0 listeners</li>
|
||||
<li class="progress_str text-muted">00:00 / 00:00</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small class="footer d-block text-muted mt-3">{{ rs.description | safe }}</small>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button data-playing="false" data-url="{{ settings.icecast2_scheme + settings.icecast2_hostname + "/" + rs.mount_point }}" data-radio="{{ rs.id }}" type="button" class="btn btn-play btn-block btn-outline-primary mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="btn_audio_icon bi bi-play" viewBox="0 0 16 16">
|
||||
<path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"></path>
|
||||
</svg>
|
||||
<span>Play</span>
|
||||
</button>
|
||||
|
||||
{% if logged_in %}
|
||||
{% if rs.id == radio_default.id %}
|
||||
<div class="btnMeta mb-2" data-url="{{ url_for('api_tune', radio_id=rs.id) }}">
|
||||
<button data-playing="false" data-radio="{{ rs.id }}" type="button" class="btn btn-block btn-outline-primary">
|
||||
<span>Tune</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<h4>Library
|
||||
<small style="font-size:12px">(by user)</small>
|
||||
</h4>
|
||||
<form method="GET" action="/library">
|
||||
<div class="input-group mb-3 style=no-gutters">
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="username...">
|
||||
<div class="input-group-append">
|
||||
<input class="btn btn-outline-secondary" type="submit" value="Search">
|
||||
<div class="btnMeta mb-2" data-url="{{ url_for('api_boo', radio_id=rs.id) }}">
|
||||
<button data-playing="false" data-radio="{{ rs.id }}" type="button" class="btn btn-block btn-outline-primary">
|
||||
<span>Boo</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="btnMeta mb-2" data-url="{{ url_for('api_skip', radio_id=rs.id) }}">
|
||||
<button data-playing="false" data-radio="{{ rs.id }}" type="button" class="btn btn-block btn-outline-primary">
|
||||
<span>Skip</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% if ENABLE_SEARCH_ROUTE %}
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h4>Quick Search
|
||||
<small style="font-size:12px">(general)</small>
|
||||
</h4>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="general" name="general" placeholder="query...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<table class="table table-sm table-hover table-bordered" id="table" style="font-size:12px">
|
||||
<thead>
|
||||
<tbody style="">
|
||||
</tbody>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<h4>IRC</h4>
|
||||
<pre>{{ settings.irc_host }}:{{ settings.irc_port }}
|
||||
{{ settings.irc_channels | join(" ") }}
|
||||
</pre>
|
||||
{% endfor %}
|
||||
|
||||
<div class="audio d-none">
|
||||
{% for rs in radio_stations %}
|
||||
<audio data-url="{{ rs.stream_url }}" id="player_{{ rs.id }}" controls preload="none">
|
||||
<source src="{{ rs.stream_url }}" type="audio/ogg">
|
||||
</audio>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
</div>
|
||||
|
||||
{% if ENABLE_SEARCH_ROUTE %}
|
||||
<script src="static/search.js"></script>
|
||||
{% endif %}
|
||||
<script>
|
||||
var radio_default_images = {
|
||||
{% for rs in radio_stations %}
|
||||
"{{ rs.id }}": "{{ url_for('static', filename=rs.image) }}",
|
||||
{% endfor %}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="static/index.js"></script>
|
||||
<script>
|
||||
var radio_data = null;
|
||||
// jquery selection caches
|
||||
var audio_np = null;
|
||||
|
||||
// lookup, to keep order
|
||||
let radio_station_ids = [{% for rs in radio_stations %}'{{ rs.id }}',{% endfor %}];
|
||||
|
||||
var sel_radio_cards = {};
|
||||
var url_icecast = '{{ settings.icecast2_hostname }}';
|
||||
var url_album_art = '/assets/art/';
|
||||
var ws_url = '{{ settings.ws_url }}';
|
||||
|
||||
var icon_play = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="btn_audio_icon bi bi-play" viewBox="0 0 16 16">
|
||||
<path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"></path>
|
||||
</svg>
|
||||
`;
|
||||
var icon_stop = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="btn_audio_icon bi bi-stop" viewBox="0 0 16 16">
|
||||
<path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5H5z"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
function stopRadio(radio_id) {
|
||||
let radio = document.getElementById("player_" + radio_id);
|
||||
radio.pause();
|
||||
radio.src = radio.src; // trick to force disconnect from radio
|
||||
}
|
||||
|
||||
function playRadio(radio_id) {
|
||||
let radio = document.getElementById("player_" + radio_id);
|
||||
let sel = $(radio);
|
||||
|
||||
radio.src = sel.attr('data-url');
|
||||
radio.play();
|
||||
}
|
||||
|
||||
function listeners_str(amount){
|
||||
if(amount >= 2 || amount === 0) return `${amount} listeners`;
|
||||
return `${amount} listener`;
|
||||
}
|
||||
|
||||
function btnPlayChangeText(radio_id, text) {
|
||||
sel_radio_cards[radio_id].find('.btn-play span').text(text);
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
for (let radio_id of radio_station_ids) {
|
||||
stopRadio(radio_id);
|
||||
btnPlayChangeActive(radio_id, false);
|
||||
btnPlayChangeText(radio_id, 'Play');
|
||||
}
|
||||
}
|
||||
|
||||
function btnPlayChangeActive(radio_id, active) {
|
||||
let _sel = sel_radio_cards[radio_id].find('.btn-play');
|
||||
_sel.find('.btn_audio_icon').remove();
|
||||
|
||||
if(active) {
|
||||
_sel.addClass('btn-active');
|
||||
_sel.attr('data-playing', 'true');
|
||||
_sel.prepend(icon_stop);
|
||||
} else {
|
||||
_sel.removeClass('btn-active');
|
||||
_sel.attr('data-playing', 'false');
|
||||
_sel.prepend(icon_play);
|
||||
}
|
||||
}
|
||||
|
||||
function labelSetListeners(radio_id) {
|
||||
if(!radio_data.hasOwnProperty(radio_id)) return;
|
||||
|
||||
let listeners = radio_data[radio_id].listeners;
|
||||
sel_radio_cards[radio_id].find('.listeners_str').text(listeners_str(listeners));
|
||||
}
|
||||
|
||||
function btnPlay(event) {
|
||||
let playing = $(event.currentTarget).attr('data-playing');
|
||||
let url = $(event.currentTarget).attr('data-url');
|
||||
let uid = $(event.currentTarget).attr('data-radio');
|
||||
|
||||
resetAll();
|
||||
|
||||
if(playing === "true" && audio_np !== null && audio_np.uid === uid) {
|
||||
stopRadio(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
playRadio(uid);
|
||||
audio_np = {'url': url, 'uid': uid}
|
||||
radio_data[uid].listeners += 1;
|
||||
|
||||
labelSetListeners(uid);
|
||||
btnPlayChangeActive(uid, true);
|
||||
btnPlayChangeText(uid, 'Pause');
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
for (let radio_station_id of radio_station_ids) {
|
||||
let _sel = $("div[data-radio=" + radio_station_id + "]");
|
||||
_sel.find('.btn-play').on('click', btnPlay); // playBtn click handler
|
||||
sel_radio_cards[radio_station_id] = _sel;
|
||||
}
|
||||
|
||||
function onData(data) {
|
||||
let blob = JSON.parse(data);
|
||||
radio_data = blob;
|
||||
|
||||
let listeners = 0;
|
||||
for (let radio_station of radio_station_ids) {
|
||||
if(!blob.hasOwnProperty(radio_station)) continue;
|
||||
|
||||
let rs = blob[radio_station];
|
||||
if(rs.song !== null) {
|
||||
let song = rs.song;
|
||||
console.log(song.album_art);
|
||||
|
||||
if(song.hasOwnProperty('image') && song.image !== null) {
|
||||
sel_radio_cards[rs.id].find('.img_header').attr('src', `${url_album_art}/${song.image}`);
|
||||
}
|
||||
|
||||
sel_radio_cards[rs.id].find('.title_str').text(song.title);
|
||||
//labelSetListeners(rs.id, rs.listeners);
|
||||
listeners += rs.listeners;
|
||||
sel_radio_cards[rs.id].find('.progress_str').text(song.progress_str);
|
||||
}
|
||||
}
|
||||
|
||||
if(listeners >= 1) {
|
||||
$('p.lead').text(listeners_str(listeners));
|
||||
} else {
|
||||
$('p.lead').text('Enjoy the music :)');
|
||||
}
|
||||
}
|
||||
|
||||
// btnMeta: skip, boo, tune
|
||||
$(document).on('click', '.btnMeta', async (ev) => {
|
||||
let sel = $(ev.currentTarget);
|
||||
let url = sel.attr('data-url');
|
||||
|
||||
fetch(url).then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error('Something went wrong');
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if(responseJson.hasOwnProperty('msg')) {
|
||||
let msg = responseJson['msg'];
|
||||
alert(msg);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(error);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
ws_connect(ws_url, onData);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,56 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ hostname }};
|
||||
root /var/www/html;
|
||||
|
||||
access_log /dev/null;
|
||||
error_log /var/log/nginx/radio_error;
|
||||
|
||||
client_max_body_size 120M;
|
||||
fastcgi_read_timeout 1600;
|
||||
proxy_read_timeout 1600;
|
||||
index index.html;
|
||||
|
||||
error_page 403 /403.html;
|
||||
location = /403.html {
|
||||
root /var/www/html;
|
||||
allow all;
|
||||
internal;
|
||||
}
|
||||
|
||||
location '/.well-known/acme-challenge' {
|
||||
default_type "text/plain";
|
||||
root /tmp/letsencrypt;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /var/www/html/;
|
||||
proxy_pass http://{{ host }}:{{ port }};
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
allow all;
|
||||
}
|
||||
|
||||
location /{{ icecast2_mount }} {
|
||||
allow all;
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
proxy_pass http://{{ icecast2_bind_host }}:{{ icecast2_bind_port }}/{{ icecast2_mount }};
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://{{ host }}:{{ port }}/ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.css">
|
||||
<script src="https://unpkg.com/bootstrap-table@1.21.3/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<style>
|
||||
.card-body .form-control {
|
||||
color: #b1b1b1;
|
||||
background-color: #171717;
|
||||
border: 1px solid #515151;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.fixed-table-pagination {display: none !important;}
|
||||
</style>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-lg-12">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Request song</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p style="color: #626262 !important;">Note: Requesting a song can take a minute or two before it starts playing.</p>
|
||||
<table
|
||||
id="songsTable"
|
||||
data-side-pagination="server"
|
||||
data-classes="table"
|
||||
data-pagination="true"
|
||||
data-toggle="table"
|
||||
data-flat="true"
|
||||
data-page-size="150"
|
||||
data-search="true"
|
||||
data-url="{{url_for('api_songs')}}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-formatter="btnMaker" data-sortable="false"></th>
|
||||
<th data-field="title" data-sortable="false">Name</th>
|
||||
<th data-field="karma" data-sortable="true">Karma</th>
|
||||
<th data-field="added_by" data-sortable="false">User</th>
|
||||
<th data-formatter="utubeMaker" data-field="utube_id" data-sortable="false"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function utubeMaker(value, row, index) {
|
||||
return `<a target="_blank" href="https://youtube.com/watch?v=${row.uid}">${row.uid}</a>`;
|
||||
}
|
||||
|
||||
function btnMaker(value, row, index) {
|
||||
return `<span class="btnRequest" style="cursor:pointer;color:#82b2e5;" data-uid="${row.uid}">request</span>`;
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
$(document).on('click', '.btnRequest', async (ev) => {
|
||||
let sel = $(ev.currentTarget);
|
||||
let uid = sel.attr('data-uid');
|
||||
|
||||
let url = "{{ url_for('api_request', utube_id='') }}" + uid;
|
||||
|
||||
fetch(url).then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error('Something went wrong');
|
||||
})
|
||||
.then((responseJson) => {
|
||||
sel.text('added');
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(error);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% include 'footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<h4>View User Library
|
||||
<small style="font-size:12px"></small>
|
||||
</h4>
|
||||
<form target="_blank" method="GET" action="/library">
|
||||
<div class="input-group mb-3 style=no-gutters">
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="username...">
|
||||
<div class="input-group-append">
|
||||
<input class="btn btn-outline-secondary" type="submit" value="Search">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|