Added misc smaller features (requirements) dsc added when he expanded to multiple channels.
This commit is contained in:
parent
5d82e02c6b
commit
c85d01cfe9
|
@ -1 +1 @@
|
|||
1797
|
||||
1846
|
125
ircradio/irc.py
125
ircradio/irc.py
|
@ -68,7 +68,7 @@ def reconnect(**kwargs):
|
|||
class Commands:
|
||||
LOOKUP = ['np', 'tune', 'boo', 'request', 'dj',
|
||||
'skip', 'listeners', 'queue',
|
||||
'queue_user', 'pop', 'search', 'stats',
|
||||
'queue_user', 'pop', 'search', 'searchq', 'stats',
|
||||
'rename', 'ban', 'whoami']
|
||||
|
||||
@staticmethod
|
||||
|
@ -116,51 +116,106 @@ class Commands:
|
|||
async def request(*args, target=None, nick=None, **kwargs):
|
||||
"""request a song by title or YouTube id"""
|
||||
from ircradio.models import Song
|
||||
|
||||
if not args:
|
||||
send_message(target=target, message="usage: !request <id>")
|
||||
await 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)
|
||||
|
||||
# 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:
|
||||
songs = Song.search(needle)
|
||||
except Exception as ex:
|
||||
return await send_message(target, f"{ex}")
|
||||
if not songs:
|
||||
return await send_message(target, "Not found!")
|
||||
|
||||
if len(songs) >= 2:
|
||||
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
|
||||
|
||||
song = songs[0]
|
||||
msg = f"Added {song.title} to the queue"
|
||||
Radio.queue(song)
|
||||
return await send_message(target, msg)
|
||||
|
||||
@staticmethod
|
||||
async def search(*args, target=None, nick=None, **kwargs):
|
||||
"""search for a title"""
|
||||
from ircradio.models import Song
|
||||
|
||||
if not args:
|
||||
return await send_message(target=target, message="usage: !search <id>")
|
||||
|
||||
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)
|
||||
if songs and needle_2nd:
|
||||
songs = [s for s in songs if s.title and needle_2nd in s.title.lower()]
|
||||
|
||||
if not songs:
|
||||
return await send_message(target, "No song(s) found after '|'!")
|
||||
|
||||
return songs
|
||||
|
||||
@staticmethod
|
||||
async def _print_song_results(*args, target=None, nick=None, report_quality=None, songs=None, **kwargs):
|
||||
from ircradio.models import Song
|
||||
|
||||
len_songs = len(songs)
|
||||
max_songs = 6
|
||||
moar = len_songs > max_songs
|
||||
if len_songs > 1:
|
||||
await send_message(target, "Multiple found:")
|
||||
for s in songs[:8]:
|
||||
await send_message(target, f"{s.utube_id} | {s.title}")
|
||||
|
||||
random.shuffle(songs)
|
||||
|
||||
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
|
||||
async def dj(*args, target=None, nick=None, **kwargs):
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Copyright (c) 2021, dsc@xmr.pm
|
||||
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
import mutagen
|
||||
import aiofiles
|
||||
|
||||
from peewee import SqliteDatabase, SQL
|
||||
import peewee as pw
|
||||
|
||||
|
@ -23,6 +29,20 @@ class Ban(pw.Model):
|
|||
class Meta:
|
||||
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):
|
||||
id = pw.AutoField()
|
||||
date_added = pw.DateTimeField(default=datetime.now)
|
||||
|
@ -34,6 +54,10 @@ class Song(pw.Model):
|
|||
karma = pw.IntegerField(default=5, index=True)
|
||||
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
|
||||
def delete_song(utube_id: str) -> bool:
|
||||
from ircradio.factory import app
|
||||
|
@ -110,6 +134,73 @@ class Song(pw.Model):
|
|||
app.logger.error(f"{ex}")
|
||||
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
|
||||
def filepath(self):
|
||||
"""Absolute"""
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<hr>
|
||||
|
||||
<audio controls src="/{{ settings.icecast2_mount }}">Your browser does not support the<code>audio</code> element.</audio>
|
||||
<audio controls src="/{{ settings.icecast2_mount }}" type="audio/mp3">Your browser does not support the<code>audio</code> element.</audio>
|
||||
<p> </p>
|
||||
|
||||
<h3>Now playing: </h3>
|
||||
|
|
|
@ -12,10 +12,14 @@ import asyncio
|
|||
from asyncio.subprocess import Process
|
||||
from io import TextIOWrapper
|
||||
|
||||
import mutagen
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import jinja2
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
from aiocache import cached, Cache
|
||||
from aiocache.serializers import PickleSerializer
|
||||
|
||||
|
||||
import settings
|
||||
|
||||
|
@ -207,6 +211,17 @@ class Price:
|
|||
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():
|
||||
print("""\033[91m ▪ ▄▄▄ ▄▄· ▄▄▄ ▄▄▄· ·▄▄▄▄ ▪
|
||||
██ ▀▄ █·▐█ ▌▪▀▄ █·▐█ ▀█ ██▪ ██ ██ ▪
|
||||
|
|
|
@ -118,8 +118,9 @@ class YouTube:
|
|||
title = 'Unknown'
|
||||
app.logger.warning(f"could not detect artist/title from metadata for {filepath}")
|
||||
|
||||
title = title if '-' in title else f"{artist} - {title}"
|
||||
return {
|
||||
"name": f"{artist} - {title}",
|
||||
"name": f"{title}",
|
||||
"data": metadata,
|
||||
"duration": duration,
|
||||
"path": filepath
|
||||
|
|
|
@ -2,6 +2,7 @@ quart
|
|||
yt-dlp
|
||||
aiofiles
|
||||
aiohttp
|
||||
aiocache
|
||||
bottom
|
||||
tinytag
|
||||
python-dateutil
|
||||
|
|
Loading…
Reference in New Issue