Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: background music, score, limit songs by user #456

Merged
merged 7 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dist/
songs/
qrcode.png
.DS_Store
config.ini
docker-compose.yml
1 change: 1 addition & 0 deletions code_quality/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ repos:
- id: requirements-txt-fixer
name: Sort requirements.txt
- id: check-added-large-files
args: [--maxkb=3000]
- id: check-case-conflict
- id: check-merge-conflict
- id: end-of-file-fixer
Expand Down
94 changes: 93 additions & 1 deletion pikaraoke/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import json
import logging
import mimetypes
import os
import re
import signal
Expand All @@ -18,6 +19,7 @@
Flask,
Response,
flash,
jsonify,
make_response,
redirect,
render_template,
Expand Down Expand Up @@ -431,6 +433,13 @@ def logo():
return send_file(k.logo_path, mimetype="image/png")


@app.route("/background_music")
def background_music():
music_path = k.bg_music_path
mime_type, _ = mimetypes.guess_type(music_path)
return send_file(k.bg_music_path, mimetype=mime_type)


@app.route("/end_song", methods=["GET", "POST"])
def end_song():
d = request.form.to_dict()
Expand Down Expand Up @@ -553,6 +562,9 @@ def splash():
hide_url=k.hide_url,
hide_overlay=k.hide_overlay,
screensaver_timeout=k.screensaver_timeout,
disable_bg_music=k.disable_bg_music,
disable_score=k.disable_score,
bg_music_volume=k.bg_music_volume,
)


Expand Down Expand Up @@ -602,6 +614,11 @@ def info():
pikaraoke_version=VERSION,
admin=is_admin(),
admin_enabled=admin_password != None,
disable_bg_music=k.disable_bg_music,
bg_music_volume=int(100 * k.bg_music_volume),
disable_score=k.disable_score,
hide_url=k.hide_url,
limit_user_songs_by=k.limit_user_songs_by,
)


Expand Down Expand Up @@ -704,7 +721,36 @@ def expand_fs():
return redirect(url_for("home"))


@app.route("/change_preferences", methods=["GET"])
def change_preferences():
if is_admin():
preference = request.args["pref"]
val = request.args["val"]

rc = k.change_preferences(preference, val)

return jsonify(rc)
else:
flash(_("You don't have permission to define audio output"), "is-danger")
return redirect(url_for("info"))


@app.route("/clear_preferences", methods=["GET"])
def clear_preferences():
if is_admin():
rc = k.clear_preferences()
if rc[0]:
flash(rc[1], "is-success")
else:
flash(rc[1], "is-danger")
else:
flash(_("You don't have permission to define audio output"), "is-danger")
return redirect(url_for("home"))


# Streams the file in chunks from the filesystem (chrome supports it, safari does not)


@app.route("/stream/<id>")
def stream(id):
file_path = f"{get_tmp_dir()}/{id}.mp4"
Expand Down Expand Up @@ -792,6 +838,7 @@ def main():
default_screensaver_delay = 300
default_log_level = logging.INFO
default_prefer_hostname = False
default_bg_music_volume = 0.3
default_buffer_size = 150000

default_dl_dir = get_default_dl_dir(platform)
Expand Down Expand Up @@ -881,7 +928,7 @@ def main():
parser.add_argument(
"--hide-overlay",
action="store_true",
help="Hide overlay that shows on top of video with pikaraoke QR code and IP",
help="Hide all overlays that show on top of video, including current/next song, pikaraoke QR code and IP",
required=False,
),
parser.add_argument(
Expand Down Expand Up @@ -950,6 +997,38 @@ def main():
default=None,
required=False,
),
parser.add_argument(
"--disable-bg-music",
action="store_true",
help="Disable background music on splash screen",
required=False,
),
parser.add_argument(
"--bg-music-volume",
default=default_bg_music_volume,
help="Set the volume of background music on splash screen. A value between 0 and 1. (default: %s)"
% default_bg_music_volume,
required=False,
),
parser.add_argument(
"--bg-music-path",
nargs="+",
help="Path to a custom background music for the splash screen. (.mp3, .wav or .ogg)",
default=None,
required=False,
),
parser.add_argument(
"--disable-score",
help="Disable the score screen after each song",
action="store_true",
required=False,
),
parser.add_argument(
"--limit-user-songs-by",
help="Limit the number of songs a user can add to queue. User name 'Pikaraoke' is always unlimited (default: 0 = unlimited)",
default="0",
required=False,
),

args = parser.parse_args()

Expand All @@ -976,6 +1055,14 @@ def main():
)
parsed_volume = default_volume

parsed_bg_volume = float(args.bg_music_volume)
if parsed_bg_volume > 1 or parsed_bg_volume < 0:
# logging.warning("BG music volume must be between 0 and 1. Setting to default: %s" % default_bg_volume)
print(
f"[ERROR] Volume: {args.bg_music_volume} must be between 0 and 1. Setting to default: {default_bg_music_volume}"
)
parsed_bg_volume = default_bg_music_volume

# Configure karaoke process
global k
k = karaoke.Karaoke(
Expand All @@ -998,6 +1085,11 @@ def main():
screensaver_timeout=args.screensaver_timeout,
url=args.url,
prefer_hostname=args.prefer_hostname,
disable_bg_music=args.disable_bg_music,
bg_music_volume=parsed_bg_volume,
bg_music_path=arg_path_parse(args.bg_music_path),
disable_score=args.disable_score,
limit_user_songs_by=int(args.limit_user_songs_by),
)
k.upgrade_youtubedl()

Expand Down
111 changes: 103 additions & 8 deletions pikaraoke/karaoke.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import configparser
import contextlib
import json
import logging
Expand All @@ -15,6 +16,7 @@
from urllib.parse import urlparse

import qrcode
from flask_babel import _
from unidecode import unidecode

from pikaraoke.lib.ffmpeg import (
Expand Down Expand Up @@ -71,6 +73,7 @@ class Karaoke:
volume = None
loop_interval = 500 # in milliseconds
default_logo_path = os.path.join(base_path, "logo.png")
default_bg_music_path = os.path.join(base_path, "static/sounds/midnight-dorufin.mp3")
screensaver_timeout = 300 # in seconds

ffmpeg_process = None
Expand All @@ -82,6 +85,8 @@ class Karaoke:
raspberry_pi = is_raspberry_pi()
os_version = get_os_version()

config_obj = configparser.ConfigParser()

def __init__(
self,
port=5555,
Expand All @@ -103,10 +108,21 @@ def __init__(
screensaver_timeout=300,
url=None,
prefer_hostname=True,
disable_bg_music=False,
bg_music_volume=0.3,
bg_music_path=None,
disable_score=False,
limit_user_songs_by=0,
):
logging.basicConfig(
format="[%(asctime)s] %(levelname)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=int(log_level),
)

# override with supplied constructor args if provided
self.port = port
self.hide_url = hide_url
self.hide_url = self.get_user_preference("hide_url") or hide_url
self.hide_notifications = hide_notifications
self.hide_raspiwifi_instructions = hide_raspiwifi_instructions
self.hide_splash_screen = hide_splash_screen
Expand All @@ -123,17 +139,18 @@ def __init__(
self.screensaver_timeout = screensaver_timeout
self.url_override = url
self.prefer_hostname = prefer_hostname
self.disable_bg_music = self.get_user_preference("disable_bg_music") or disable_bg_music
self.bg_music_volume = self.get_user_preference("bg_music_volume") or bg_music_volume
self.bg_music_path = self.default_bg_music_path if bg_music_path == None else bg_music_path
self.disable_score = self.get_user_preference("disable_score") or disable_score
self.limit_user_songs_by = (
self.get_user_preference("limit_user_songs_by") or limit_user_songs_by
)

# other initializations
self.platform = get_platform()
self.screen = None

logging.basicConfig(
format="[%(asctime)s] %(levelname)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=int(log_level),
)

logging.debug(
f"""
http port: {self.port}
Expand All @@ -154,6 +171,11 @@ def __init__(
logo path: {self.logo_path}
log_level: {log_level}
hide overlay: {self.hide_overlay}
disable bg music: {self.disable_bg_music}
bg music volume: {self.bg_music_volume}
bg music path: {self.bg_music_path}
disable score: {self.disable_score}
limit user songs by: {self.limit_user_songs_by}
hide notifications: {self.hide_notifications}
platform: {self.platform}
Expand All @@ -164,6 +186,7 @@ def __init__(
youtubedl-version: {self.get_youtubedl_version()}
"""
)

# Generate connection URL and QR code,
if self.raspberry_pi:
# retry in case pi is still starting up
Expand Down Expand Up @@ -199,6 +222,62 @@ def __init__(

self.generate_qr_code()

# def get_user_preferences(self, preference):
def get_user_preference(self, preference, default_value=False):
# Try to read the config file
try:
self.config_obj.read("config.ini")
except FileNotFoundError:
return default_value

# Check if the section exists
if not self.config_obj.has_section("USERPREFERENCES"):
return default_value

# Try to get the value
try:
pref = self.config_obj.get("USERPREFERENCES", preference)
if pref == "True":
return True
elif pref == "False":
return False
elif pref.isnumeric():
return int(pref)
elif pref.replace(".", "", 1).isdigit():
return float(pref)
else:
return pref

except (configparser.NoOptionError, ValueError):
return default_value

def change_preferences(self, preference, val):
"""Makes changes in the config.ini file that stores the user preferences.
Receives the preference and it's new value"""

logging.debug("Changing user preference << %s >> to %s" % (preference, val))
try:
if "USERPREFERENCES" not in self.config_obj:
self.config_obj.add_section("USERPREFERENCES")

userprefs = self.config_obj["USERPREFERENCES"]
userprefs[preference] = str(val)
setattr(self, preference, eval(str(val)))
with open("config.ini", "w") as conf:
self.config_obj.write(conf)
self.changed_preferences = True
return [True, _("Your preferences were changed successfully")]
except Exception as e:
logging.debug("Failed to change user preference << %s >>: %s", preference, e)
return [False, _("Something went wrong! Your preferences were not changed")]

def clear_preferences(self):
try:
os.remove("config.ini")
return [True, _("Your preferences were cleared successfully")]
except OSError:
return [False, _("Something went wrong! Your preferences were not cleared")]

def get_ip(self):
# python socket.connect will not work on android, access denied. Workaround: use ifconfig which is installed to termux by default, iirc.
if self.platform == "android":
Expand Down Expand Up @@ -594,12 +673,28 @@ def is_song_in_queue(self, song_path):
return True
return False

def is_user_limited(self, user):
# Returns if a user needs to be limited or not if the limitation is on and if the user reached the limit of songs in queue
if self.limit_user_songs_by == 0 or user == "Pikaraoke" or user == "Randomizer":
return False
cont = len([i for i in self.queue if i["user"] == user]) + (
1 if self.now_playing_user == user else 0
)
return True if cont >= int(self.limit_user_songs_by) else False

def enqueue(
self, song_path, user="Pikaraoke", semitones=0, add_to_front=False, log_action=True
):
if self.is_song_in_queue(song_path):
logging.warning("Song is already in queue, will not add: " + song_path)
return False
elif self.is_user_limited(user):
logging.debug("User limitted by: " + str(self.limit_user_songs_by))
return [
False,
_("You reached the limit of %s song(s) from an user in queue!")
% (str(self.limit_user_songs_by)),
]
else:
queue_item = {
"user": user,
Expand All @@ -614,7 +709,7 @@ def enqueue(
if log_action:
self.log_and_send(f"{user} added to the queue: {queue_item['title']}", "info")
self.queue.append(queue_item)
return True
return [True, _("Song added to the queue: %s") % (self.filename_from_path(song_path))]

def queue_add_random(self, amount):
logging.info("Adding %d random songs to queue" % amount)
Expand Down
Loading
Loading