You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
196 lines
5.9 KiB
196 lines
5.9 KiB
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
|
|
|
|
# find a better way to get current song
|
|
liq_filenames = []
|
|
from ircradio.factory import NP_MAP
|
|
np_uid = self.id
|
|
if np_uid == "main":
|
|
np_uid = "pmain"
|
|
elif np_uid == "wow":
|
|
np_uid = "pmain"
|
|
|
|
if np_uid in NP_MAP:
|
|
liq_filenames = [NP_MAP[np_uid]]
|
|
|
|
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"now_playing"
|
|
|
|
@property
|
|
def telnet_cmd_remaining(self):
|
|
return f"{self.mount_point.replace('.', '_')}.remaining"
|
|
|
|
@property
|
|
def telnet_cmd_skip(self):
|
|
return f"{self.mount_point.replace('.', '_')}.skip"
|
|
|
|
@property
|
|
def stream_url(self):
|
|
return f"http://{settings.icecast2_hostname}/{self.mount_point}"
|