Compare commits

...

6 Commits

Author SHA1 Message Date
scoobybejesus 665468a1a6 So many changes in one commit. 2023-02-05 18:33:57 +00:00
scoobybejesus 04fd2e8695 update gitignore 2022-06-22 02:53:00 +00:00
scoobybejesus 09e6cfb960 Merge branch 'master' of https://git.wownero.com/dsc/ircradio 2021-08-05 23:49:22 +00:00
scoobybejesus 09bc26d778 Personalize 2021-08-05 23:49:14 +00:00
scoobybejesus a72da591e9 Remove check 2021-08-05 23:48:32 +00:00
dsc c0a1415eae Add JSON API for searches. Introduces new settings.py config option 'enable_search_route' 2021-08-04 18:53:56 +02:00
12 changed files with 289 additions and 31 deletions

6
.gitignore vendored
View File

@ -4,3 +4,9 @@ data/music/*.webp
data/music/*.ogg* data/music/*.ogg*
__pycache__ __pycache__
settings.py settings.py
data/*
!data/agents.txt
venv/
ircradio/static/favicons/
ircradio/favicon.ico
ircradio/site.manifest

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

1
ircradio.pid Normal file
View File

@ -0,0 +1 @@
1797

View File

@ -1,9 +1,13 @@
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2021, dsc@xmr.pm # Copyright (c) 2021, dsc@xmr.pm
import asyncio
from datetime import datetime from datetime import datetime
from typing import Tuple, Optional from typing import Tuple, Optional
from quart import request, render_template, abort from quart import request, render_template, abort, jsonify, Response, websocket
import time
import json
import settings import settings
from ircradio.factory import app from ircradio.factory import app
@ -39,6 +43,52 @@ async def history():
return data return data
@app.route("/search")
async def search():
# search json api endpoint
# e.g: /search?name=test&limit=5&offset=0
if not settings.enable_search_route:
abort(404)
from ircradio.models import Song
name = request.args.get("name")
limit = request.args.get("limit", '20')
offset = request.args.get("offset", '0')
try:
limit = int(limit)
offset = int(offset)
except:
limit = 50
offset = 0
if not name or len(name) <= 2:
abort(404)
if limit > 50:
limit = 50
name = f"%{name}%"
try:
q = Song.select()
q = q.where((Song.added_by ** name) | (Song.title ** name))
q = q.order_by(Song.date_added.desc())
q = q.limit(limit).offset(offset)
results = [{
"added_by": s.added_by,
"karma": s.karma,
"id": s.id,
"title": s.title,
"utube_id": s.utube_id,
"date_added": s.date_added.strftime("%Y-%m-%d")
} for s in q]
except:
return jsonify([])
return jsonify(results)
@app.route("/library") @app.route("/library")
async def user_library(): async def user_library():
from ircradio.models import Song from ircradio.models import Song
@ -62,3 +112,27 @@ async def user_library():
by_karma = [] by_karma = []
return await render_template("library.html", name=name, by_date=by_date, by_karma=by_karma) return await render_template("library.html", name=name, by_date=by_date, by_karma=by_karma)
@app.websocket("/ws")
async def np():
last_song = ""
while True:
"""get current song from history"""
history = Radio.history()
val = ""
if not history:
val = f"Nothing is playing?!"
else:
song = history[0]
val = song.title
if val != last_song:
data = json.dumps({"now_playing": val})
await websocket.send(f"{data}")
last_song = val
await asyncio.sleep(5)

19
ircradio/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "IRC!Radio ala Scoob!",
"short_name": "IRC!Radio ala Scoob!",
"icons": [
{
"src": "/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

79
ircradio/static/search.js Normal file
View File

@ -0,0 +1,79 @@
// 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();
});

View File

@ -27,9 +27,45 @@
<meta name="application-name" content="IRC!Radio"> <meta name="application-name" content="IRC!Radio">
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="description" content="IRC!Radio"/> <meta name="description" content="IRC!Radio"/>
<title>IRC!Radio</title> <title>IRC!Radio ala Scoob!</title>
<link rel="apple-touch-icon" sizes="180x180" href="static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/favicons/favicon-16x16.png">
<!-- <link rel="manifest" href="/site.webmanifest"> -->
<link rel="mask-icon" href="static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"></link>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
<script>
var ws = new WebSocket('wss://' + document.domain + ':' + location.port + '/ws');
ws.onmessage = function (event) {
// console.log(event.data);
json = JSON.parse(event.data);
np = json.now_playing;
document.querySelector("#prev_two").innerText = document.querySelector("#prev_one").innerText;
document.querySelector("#prev_one").innerText = document.querySelector("#now_playing").innerText;
document.querySelector("#now_playing").innerText = np;
console.log(np);
return false;
};
ws.onclose = function(event) {
console.log("WebSocket is closed now.");
};
</script>
</head> </head>
<body> <body>

View File

@ -4,14 +4,28 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<!-- Post Content Column --> <!-- Post Content Column -->
<div class="col-lg-12"> <div class="col-lg-9">
<!-- Title --> <!-- Title -->
<h1 class="mt-4" style="margin-bottom: 2rem;"> <h1 class="mt-4" style="margin-bottom: 0rem;">
IRC!Radio IRC!Radio ala Scoob!
</h1> </h1>
<h5 class="mt-4" style="margin-bottom: 1rem;">
Thanks, dsc_!
</h5>
<p>Enjoy the music :)</p> <p>Enjoy the music :)</p>
<hr> <hr>
<audio controls src="/{{ settings.icecast2_mount }}">Your browser does not support the<code>audio</code> element.</audio> <audio controls src="/{{ settings.icecast2_mount }}">Your browser does not support the<code>audio</code> element.</audio>
<p> </p>
<h3>Now playing: </h3>
<div id="now_playing" style="padding-top:6px; margin-bottom: 1.75rem;"></div>
<h5>Previous: </h5>
<div id="prev_one" style="font-size:12px;">Nothing here yet</div>
<div id="prev_two" style="font-size:12px;"></div>
<hr> <hr>
<h4>Command list:</h4> <h4>Command list:</h4>
@ -32,29 +46,52 @@
</pre> </pre>
<hr> <hr>
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<h4>History</h4> <h4>History</h4>
<a href="/history.txt">history.txt</a> <a href="/history.txt" target="_blank">View in new tab</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-5">
<h4>Library
<small style="font-size:12px">(by user)</small> <h4>View User Library
<small style="font-size:12px"></small>
</h4> </h4>
<form method="GET" action="/library"> <form target="_blank" method="GET" action="/library">
<div class="input-group mb-3"> <div class="input-group mb-3 style=no-gutters">
<input type="text" class="form-control" id="name" name="name" placeholder="username..."> <input type="text" class="form-control" id="name" name="name" placeholder="username...">
<div class="input-group-append"> <div class="input-group-append">
<input class="btn btn-outline-secondary" type="submit" value="Search"> <input class="btn btn-outline-secondary" type="submit" value="Search">
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<div class="col-md-3"></div>
</div> </div>
<hr> <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>
<hr>
<h4>IRC</h4> <h4>IRC</h4>
<pre>{{ settings.irc_host }}:{{ settings.irc_port }} <pre>{{ settings.irc_host }}:{{ settings.irc_port }}
{{ settings.irc_channels | join(" ") }} {{ settings.irc_channels | join(" ") }}
@ -62,4 +99,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="static/search.js"></script>
{% endblock %} {% endblock %}

View File

@ -155,18 +155,18 @@ def liquidsoap_version():
def liquidsoap_check_symlink(): def liquidsoap_check_symlink():
msg = """ # msg = """
Due to a bug you need to create this symlink: # Due to a bug you need to create this symlink:
#
$ sudo ln -s /usr/share/liquidsoap/ /usr/share/liquidsoap/1.4.1 # $ sudo ln -s /usr/share/liquidsoap/ /usr/share/liquidsoap/1.4.1
#
info: https://github.com/savonet/liquidsoap/issues/1224 # info: https://github.com/savonet/liquidsoap/issues/1224
""" # """
version = liquidsoap_version() # version = liquidsoap_version()
if not os.path.exists(f"/usr/share/liquidsoap/{version}"): # if not os.path.exists(f"/usr/share/liquidsoap/{version}"):
print(msg) # print(msg)
sys.exit() # sys.exit()
pass
async def httpget(url: str, json=True, timeout: int = 5, raise_for_status=True, verify_tls=True): async def httpget(url: str, json=True, timeout: int = 5, raise_for_status=True, verify_tls=True):
headers = {"User-Agent": random_agent()} headers = {"User-Agent": random_agent()}

View File

@ -36,7 +36,9 @@ class YouTube:
try: try:
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
*["youtube-dl", *[
#"/home/radio/ircradio/venv/bin/youtube-dl",
"youtube-dl",
"--add-metadata", "--add-metadata",
"--write-all-thumbnails", "--write-all-thumbnails",
"--write-info-json", "--write-info-json",

View File

@ -4,7 +4,6 @@ aiofiles
aiohttp aiohttp
bottom bottom
tinytag tinytag
peewee
python-dateutil python-dateutil
mutagen mutagen
peewee peewee

View File

@ -16,6 +16,8 @@ timezone = "Europe/Amsterdam"
dir_music = os.environ.get("DIR_MUSIC", os.path.join(cwd, "data", "music")) dir_music = os.environ.get("DIR_MUSIC", os.path.join(cwd, "data", "music"))
enable_search_route = bool_env(os.environ.get("ENABLE_SEARCH_ROUTE", False))
irc_admins_nicknames = ["dsc_"] irc_admins_nicknames = ["dsc_"]
irc_host = os.environ.get('IRC_HOST', 'localhost') irc_host = os.environ.get('IRC_HOST', 'localhost')
irc_port = int(os.environ.get('IRC_PORT', 6667)) irc_port = int(os.environ.get('IRC_PORT', 6667))