commits
tags
import subprocess
from pathlib import Path
from flask import Blueprint, jsonify, request, abort
import db
import extensions
from auth import login_required
from config import ALLOWED_ROOTS
from utils.youtube import yt_extract, playlist_cache as _yt_playlist_cache
bp = Blueprint('music', __name__, url_prefix='/api')
_MUSIC_EXTS = {'.mp3', '.flac', '.ogg', '.wav', '.m4a', '.aac'}
_music_playlist_cache = []
@bp.route('/music', methods=['GET'])
def get_music():
state = db.get_state()
return jsonify({
'enabled': state.get('music_enabled', '0') == '1',
'source_type': state.get('music_source_type', 'local'),
'source': state.get('music_source', ''),
'volume': int(state.get('music_volume', '50')),
'shuffle': state.get('music_shuffle', '0') == '1',
'show_overlay': state.get('music_show_overlay', '1') == '1',
'overlay_position': state.get('music_overlay_position', 'bottom-left'),
'now_playing': {
'title': state.get('music_now_title', ''),
'artist': state.get('music_now_artist', ''),
},
})
@bp.route('/music', methods=['POST'])
@login_required
def set_music():
data = request.get_json()
if not data:
abort(400)
enabled = bool(data.get('enabled', False))
source_type = 'youtube' if data.get('source_type') == 'youtube' else 'local'
source = str(data.get('source', ''))
volume = max(0, min(100, int(data.get('volume', 50))))
shuffle = bool(data.get('shuffle', False))
show_overlay = bool(data.get('show_overlay', True))
overlay_position = str(data.get('overlay_position', 'bottom-left'))
if overlay_position not in {'top-left', 'top-right', 'bottom-left', 'bottom-right'}:
overlay_position = 'bottom-left'
db.set_state('music_enabled', '1' if enabled else '0')
db.set_state('music_source_type', source_type)
db.set_state('music_source', source)
db.set_state('music_volume', str(volume))
db.set_state('music_shuffle', '1' if shuffle else '0')
db.set_state('music_show_overlay', '1' if show_overlay else '0')
db.set_state('music_overlay_position', overlay_position)
payload = {
'enabled': enabled, 'source_type': source_type,
'source': source, 'volume': volume, 'shuffle': shuffle,
'show_overlay': show_overlay, 'overlay_position': overlay_position,
}
extensions.socketio.emit('music_settings', payload)
return '', 204
@bp.route('/music/tracks', methods=['GET'])
@login_required
def get_music_tracks():
folder = request.args.get('path', '')
if not folder:
return jsonify([])
p = Path(folder)
try:
p = p.resolve()
if not any(str(p).startswith(str(r)) for r in ALLOWED_ROOTS):
abort(403)
except Exception:
abort(400)
if not p.is_dir():
return jsonify([])
tracks = sorted([
{'name': f.name, 'path': str(f)}
for f in p.iterdir()
if f.is_file() and f.suffix.lower() in _MUSIC_EXTS
], key=lambda x: x['name'])
return jsonify(tracks)
@bp.route('/music/extract', methods=['POST'])
@login_required
def extract_music_playlist():
"""Extract video IDs for the music playlist and persist to DB."""
global _music_playlist_cache
data = request.get_json() or {}
url = str(data.get('url', '')).strip()
if not url:
return jsonify({'error': 'URL saknas'}), 400
try:
ids, _total_seconds = yt_extract(url)
except subprocess.TimeoutExpired:
return jsonify({'error': 'Timeout — spellistans hämtning tog för lång tid'}), 504
except Exception as e:
return jsonify({'error': str(e)}), 500
_music_playlist_cache = ids
_yt_playlist_cache[url] = ids
db.set_state('music_playlist_ids', ','.join(ids))
return jsonify({'count': len(ids), 'ids': ids})
@bp.route('/music/playlist', methods=['GET'])
def get_music_playlist():
"""Return the extracted playlist video IDs."""
global _music_playlist_cache
if not _music_playlist_cache:
stored = db.get_state().get('music_playlist_ids', '')
_music_playlist_cache = [v for v in stored.split(',') if v] if stored else []
return jsonify({'ids': _music_playlist_cache, 'count': len(_music_playlist_cache)})
@bp.route('/music/nowplaying', methods=['POST'])
def update_now_playing():
"""Called by display client to broadcast now-playing state to admin."""
data = request.get_json() or {}
title = str(data.get('title', ''))[:200]
artist = str(data.get('artist', ''))[:200]
db.set_state('music_now_title', title)
db.set_state('music_now_artist', artist)
extensions.socketio.emit('now_playing', {'title': title, 'artist': artist})
return '', 204
# --- YouTube playlist extraction (video & musik) ---
def _playlist_db_key(url):
import hashlib
return 'playlist_ids_' + hashlib.md5(url.encode()).hexdigest()
@bp.route('/playlist/extract', methods=['POST'])
@login_required
def extract_playlist():
"""Extract video IDs from any public YouTube playlist URL and cache by URL."""
data = request.get_json() or {}
url = str(data.get('url', '')).strip()
if not url:
return jsonify({'error': 'URL saknas'}), 400
try:
ids, total_seconds = yt_extract(url)
except subprocess.TimeoutExpired:
return jsonify({'error': 'Timeout'}), 504
except Exception as e:
return jsonify({'error': str(e)}), 500
_yt_playlist_cache[url] = ids
db.set_state(_playlist_db_key(url), ','.join(ids))
return jsonify({'count': len(ids), 'ids': ids, 'total_seconds': total_seconds})
@bp.route('/playlist/ids', methods=['GET'])
def get_playlist_ids():
"""Return cached video IDs for a playlist URL (memory cache, then DB)."""
url = request.args.get('url', '').strip()
ids = _yt_playlist_cache.get(url)
if not ids:
stored = db.get_state().get(_playlist_db_key(url), '')
ids = [v for v in stored.split(',') if v] if stored else []
if ids:
_yt_playlist_cache[url] = ids
return jsonify({'ids': ids or [], 'count': len(ids or [])})