Compare commits
No commits in common. "c85d01cfe9737dbf1b8d26b85a0e0666a97ebe17" and "4375c0d0a39b4387d4f9f0d2d4b94cdf0236a51d" have entirely different histories.
c85d01cfe9
...
4375c0d0a3
|
@ -10,4 +10,3 @@ venv/
|
||||||
ircradio/static/favicons/
|
ircradio/static/favicons/
|
||||||
ircradio/favicon.ico
|
ircradio/favicon.ico
|
||||||
ircradio/site.manifest
|
ircradio/site.manifest
|
||||||
ircradio.pid
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1846
|
1797
|
117
ircradio/irc.py
117
ircradio/irc.py
|
@ -68,7 +68,7 @@ def reconnect(**kwargs):
|
||||||
class Commands:
|
class Commands:
|
||||||
LOOKUP = ['np', 'tune', 'boo', 'request', 'dj',
|
LOOKUP = ['np', 'tune', 'boo', 'request', 'dj',
|
||||||
'skip', 'listeners', 'queue',
|
'skip', 'listeners', 'queue',
|
||||||
'queue_user', 'pop', 'search', 'searchq', 'stats',
|
'queue_user', 'pop', 'search', 'stats',
|
||||||
'rename', 'ban', 'whoami']
|
'rename', 'ban', 'whoami']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -116,106 +116,51 @@ class Commands:
|
||||||
async def request(*args, target=None, nick=None, **kwargs):
|
async def request(*args, target=None, nick=None, **kwargs):
|
||||||
"""request a song by title or YouTube id"""
|
"""request a song by title or YouTube id"""
|
||||||
from ircradio.models import Song
|
from ircradio.models import Song
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
await send_message(target=target, message="usage: !request <id>")
|
send_message(target=target, message="usage: !request <id>")
|
||||||
|
|
||||||
songs = await Commands._return_song_results(*args, target=target, nick=nick, **kwargs)
|
|
||||||
|
|
||||||
if songs and len(songs) == 1:
|
|
||||||
song = songs[0]
|
|
||||||
Radio.queue(song)
|
|
||||||
msg = f"Added {song.title} to the queue"
|
|
||||||
|
|
||||||
return await send_message(target, msg)
|
|
||||||
|
|
||||||
if songs:
|
|
||||||
return await Commands._print_song_results(*args, target=target, nick=nick, songs=songs, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def search(*args, target=None, nick=None, **kwargs):
|
|
||||||
from ircradio.models import Song
|
|
||||||
if not args:
|
|
||||||
return await send_message(target=target, message="usage: !search <id>")
|
|
||||||
|
|
||||||
return await Commands._search(*args, target=target, nick=nick, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def searchq(*args, target=None, nick=None, **kwargs):
|
|
||||||
from ircradio.models import Song
|
|
||||||
if not args:
|
|
||||||
return await send_message(target=target, message="usage: !searchq <id>")
|
|
||||||
|
|
||||||
return await Commands._search(*args, target=target, nick=nick, report_quality=True, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _search(*args, target=None, nick=None, **kwargs) -> Optional[List['Song']]:
|
|
||||||
"""search for a title"""
|
|
||||||
from ircradio.models import Song
|
|
||||||
|
|
||||||
report_quality = kwargs.get('report_quality')
|
|
||||||
|
|
||||||
songs = await Commands._return_song_results(*args, target=target, nick=nick, **kwargs)
|
|
||||||
|
|
||||||
if songs:
|
|
||||||
return await Commands._print_song_results(*args, target=target, nick=nick, report_quality=report_quality, songs=songs, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _return_song_results(*args, target=None, nick=None, **kwargs) -> Optional[List['Song']]:
|
|
||||||
from ircradio.models import Song
|
|
||||||
|
|
||||||
needle = " ".join(args)
|
needle = " ".join(args)
|
||||||
|
|
||||||
# https://git.wownero.com/dsc/ircradio/issues/1
|
|
||||||
needle_2nd = None
|
|
||||||
if "|" in needle:
|
|
||||||
spl = needle.split('|', 1)
|
|
||||||
a = spl[0].strip()
|
|
||||||
b = spl[1].strip()
|
|
||||||
needle = a
|
|
||||||
needle_2nd = b
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
songs = Song.search(needle)
|
songs = Song.search(needle)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
return await send_message(target, f"{ex}")
|
return await send_message(target, f"{ex}")
|
||||||
if not songs:
|
if not songs:
|
||||||
return await send_message(target, "No song(s) found!")
|
return await send_message(target, "Not found!")
|
||||||
|
|
||||||
if songs and needle_2nd:
|
if len(songs) >= 2:
|
||||||
songs = [s for s in songs if s.title and needle_2nd in s.title.lower()]
|
random.shuffle(songs)
|
||||||
|
await send_message(target, "Multiple found:")
|
||||||
|
for s in songs[:4]:
|
||||||
|
await send_message(target, f"{s.utube_id} | {s.title}")
|
||||||
|
return
|
||||||
|
|
||||||
if not songs:
|
song = songs[0]
|
||||||
return await send_message(target, "No song(s) found after '|'!")
|
msg = f"Added {song.title} to the queue"
|
||||||
|
Radio.queue(song)
|
||||||
return songs
|
return await send_message(target, msg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _print_song_results(*args, target=None, nick=None, report_quality=None, songs=None, **kwargs):
|
async def search(*args, target=None, nick=None, **kwargs):
|
||||||
|
"""search for a title"""
|
||||||
from ircradio.models import Song
|
from ircradio.models import Song
|
||||||
|
|
||||||
len_songs = len(songs)
|
if not args:
|
||||||
max_songs = 6
|
return await send_message(target=target, message="usage: !search <id>")
|
||||||
moar = len_songs > max_songs
|
|
||||||
if len_songs > 1:
|
needle = " ".join(args)
|
||||||
|
songs = Song.search(needle)
|
||||||
|
if not songs:
|
||||||
|
return await send_message(target, "No song(s) found!")
|
||||||
|
|
||||||
|
if len(songs) == 1:
|
||||||
|
song = songs[0]
|
||||||
|
await send_message(target, f"{song.utube_id} | {song.title}")
|
||||||
|
else:
|
||||||
|
random.shuffle(songs)
|
||||||
await send_message(target, "Multiple found:")
|
await send_message(target, "Multiple found:")
|
||||||
|
for s in songs[:8]:
|
||||||
random.shuffle(songs)
|
await send_message(target, f"{s.utube_id} | {s.title}")
|
||||||
|
|
||||||
for s in songs[:max_songs]:
|
|
||||||
msg = f"{s.utube_id} | {s.title}"
|
|
||||||
await s.scan(s.path or s.filepath)
|
|
||||||
|
|
||||||
if report_quality and s.meta:
|
|
||||||
if s.meta.bitrate:
|
|
||||||
msg += f" ({s.meta.bitrate / 1000}kbps)"
|
|
||||||
if s.meta.channels:
|
|
||||||
msg += f" (channels: {s.meta.channels}) "
|
|
||||||
if s.meta.sample_rate:
|
|
||||||
msg += f" (sample_rate: {s.meta.sample_rate}) "
|
|
||||||
await send_message(target, msg)
|
|
||||||
|
|
||||||
if moar:
|
|
||||||
await send_message(target, "[...]")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def dj(*args, target=None, nick=None, **kwargs):
|
async def dj(*args, target=None, nick=None, **kwargs):
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
# 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 logging
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
import mutagen
|
import mutagen
|
||||||
import aiofiles
|
|
||||||
|
|
||||||
from peewee import SqliteDatabase, SQL
|
from peewee import SqliteDatabase, SQL
|
||||||
import peewee as pw
|
import peewee as pw
|
||||||
|
|
||||||
|
@ -29,20 +23,6 @@ class Ban(pw.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SongMeta:
|
|
||||||
title: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
artist: Optional[str] = None
|
|
||||||
album: Optional[str] = None
|
|
||||||
album_cover: Optional[str] = None # path to image
|
|
||||||
bitrate: Optional[int] = None
|
|
||||||
length: Optional[float] = None
|
|
||||||
sample_rate: Optional[int] = None
|
|
||||||
channels: Optional[int] = None
|
|
||||||
mime: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Song(pw.Model):
|
class Song(pw.Model):
|
||||||
id = pw.AutoField()
|
id = pw.AutoField()
|
||||||
date_added = pw.DateTimeField(default=datetime.now)
|
date_added = pw.DateTimeField(default=datetime.now)
|
||||||
|
@ -54,10 +34,6 @@ class Song(pw.Model):
|
||||||
karma = pw.IntegerField(default=5, index=True)
|
karma = pw.IntegerField(default=5, index=True)
|
||||||
banned = pw.BooleanField(default=False)
|
banned = pw.BooleanField(default=False)
|
||||||
|
|
||||||
meta: SongMeta = None # directly from file (exif) or metadata json
|
|
||||||
path: Optional[str] = None
|
|
||||||
remaining: int = None # liquidsoap playing status in seconds
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_song(utube_id: str) -> bool:
|
def delete_song(utube_id: str) -> bool:
|
||||||
from ircradio.factory import app
|
from ircradio.factory import app
|
||||||
|
@ -134,73 +110,6 @@ class Song(pw.Model):
|
||||||
app.logger.error(f"{ex}")
|
app.logger.error(f"{ex}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def scan(self, path: str = None):
|
|
||||||
# update with metadata, etc.
|
|
||||||
if path is None:
|
|
||||||
path = self.filepath
|
|
||||||
|
|
||||||
if not os.path.exists(path):
|
|
||||||
raise Exception(f"filepath {path} does not exist")
|
|
||||||
basename = os.path.splitext(os.path.basename(path))[0]
|
|
||||||
|
|
||||||
self.meta = SongMeta()
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
# EXIF direct
|
|
||||||
from ircradio.utils import mutagen_file
|
|
||||||
_m = await mutagen_file(path)
|
|
||||||
|
|
||||||
if _m:
|
|
||||||
if _m.info:
|
|
||||||
self.meta.channels = _m.info.channels
|
|
||||||
self.meta.length = int(_m.info.length)
|
|
||||||
if hasattr(_m.info, 'bitrate'):
|
|
||||||
self.meta.bitrate = _m.info.bitrate
|
|
||||||
if hasattr(_m.info, 'sample_rate'):
|
|
||||||
self.meta.sample_rate = _m.info.sample_rate
|
|
||||||
if _m.tags:
|
|
||||||
if hasattr(_m.tags, 'as_dict'):
|
|
||||||
_tags = _m.tags.as_dict()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
_tags = {k: v for k, v in _m.tags.items()}
|
|
||||||
except:
|
|
||||||
_tags = {}
|
|
||||||
|
|
||||||
for k, v in _tags.items():
|
|
||||||
if isinstance(v, list):
|
|
||||||
v = v[0]
|
|
||||||
elif isinstance(v, (str, int, float)):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if k in ["title", "description", "language", "date", "purl", "artist"]:
|
|
||||||
if hasattr(self.meta, k):
|
|
||||||
setattr(self.meta, k, v)
|
|
||||||
|
|
||||||
# yt-dlp metadata json file
|
|
||||||
fn_utube_meta = os.path.join(settings.dir_meta, f"{basename}.info.json")
|
|
||||||
utube_meta = {}
|
|
||||||
if os.path.exists(fn_utube_meta):
|
|
||||||
async with aiofiles.open(fn_utube_meta, mode="r") as f:
|
|
||||||
try:
|
|
||||||
utube_meta = json.loads(await f.read())
|
|
||||||
except Exception as ex:
|
|
||||||
logging.error(f"could not parse {fn_utube_meta}, {ex}")
|
|
||||||
|
|
||||||
if utube_meta:
|
|
||||||
# utube_meta file does not have anything we care about
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not self.title and not self.meta.title:
|
|
||||||
# just adopt filename
|
|
||||||
self.title = os.path.basename(self.path)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filepath(self):
|
def filepath(self):
|
||||||
"""Absolute"""
|
"""Absolute"""
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<audio controls src="/{{ settings.icecast2_mount }}" type="audio/mp3">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>
|
<p> </p>
|
||||||
|
|
||||||
<h3>Now playing: </h3>
|
<h3>Now playing: </h3>
|
||||||
|
|
|
@ -12,14 +12,10 @@ import asyncio
|
||||||
from asyncio.subprocess import Process
|
from asyncio.subprocess import Process
|
||||||
from io import TextIOWrapper
|
from io import TextIOWrapper
|
||||||
|
|
||||||
import mutagen
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import jinja2
|
import jinja2
|
||||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
from aiocache import cached, Cache
|
|
||||||
from aiocache.serializers import PickleSerializer
|
|
||||||
|
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
|
|
||||||
|
@ -211,17 +207,6 @@ class Price:
|
||||||
return blob.get('usd', 0)
|
return blob.get('usd', 0)
|
||||||
|
|
||||||
|
|
||||||
#@cached(ttl=3600, cache=Cache.MEMORY,
|
|
||||||
# key_builder=lambda *args, **kw: f"mutagen_file_{args[1]}",
|
|
||||||
# serializer=PickleSerializer())
|
|
||||||
async def mutagen_file(path):
|
|
||||||
from quart import current_app
|
|
||||||
if current_app:
|
|
||||||
return await current_app.sync_to_async(mutagen.File)(path)
|
|
||||||
else:
|
|
||||||
return mutagen.File(path)
|
|
||||||
|
|
||||||
|
|
||||||
def print_banner():
|
def print_banner():
|
||||||
print("""\033[91m ▪ ▄▄▄ ▄▄· ▄▄▄ ▄▄▄· ·▄▄▄▄ ▪
|
print("""\033[91m ▪ ▄▄▄ ▄▄· ▄▄▄ ▄▄▄· ·▄▄▄▄ ▪
|
||||||
██ ▀▄ █·▐█ ▌▪▀▄ █·▐█ ▀█ ██▪ ██ ██ ▪
|
██ ▀▄ █·▐█ ▌▪▀▄ █·▐█ ▀█ ██▪ ██ ██ ▪
|
||||||
|
|
|
@ -118,9 +118,8 @@ class YouTube:
|
||||||
title = 'Unknown'
|
title = 'Unknown'
|
||||||
app.logger.warning(f"could not detect artist/title from metadata for {filepath}")
|
app.logger.warning(f"could not detect artist/title from metadata for {filepath}")
|
||||||
|
|
||||||
title = title if '-' in title else f"{artist} - {title}"
|
|
||||||
return {
|
return {
|
||||||
"name": f"{title}",
|
"name": f"{artist} - {title}",
|
||||||
"data": metadata,
|
"data": metadata,
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
"path": filepath
|
"path": filepath
|
||||||
|
|
|
@ -2,7 +2,6 @@ quart
|
||||||
yt-dlp
|
yt-dlp
|
||||||
aiofiles
|
aiofiles
|
||||||
aiohttp
|
aiohttp
|
||||||
aiocache
|
|
||||||
bottom
|
bottom
|
||||||
tinytag
|
tinytag
|
||||||
python-dateutil
|
python-dateutil
|
||||||
|
|
Loading…
Reference in New Issue