Added misc smaller features (requirements) dsc added when he expanded to multiple channels.

This commit is contained in:
scoob radio 2023-10-19 22:29:56 +00:00 committed by matt
parent 5d82e02c6b
commit c85d01cfe9
7 changed files with 201 additions and 38 deletions

View File

@ -1 +1 @@
1797
1846

View File

@ -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):

View File

@ -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"""

View File

@ -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>

View File

@ -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 ▪ ▄▄▄ ▄▄· ▄▄▄ ▄▄▄· ·▄▄▄▄ ▪
· ·

View File

@ -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

View File

@ -2,6 +2,7 @@ quart
yt-dlp
aiofiles
aiohttp
aiocache
bottom
tinytag
python-dateutil