Compare commits

...

5 Commits

12 changed files with 1192 additions and 266 deletions

View File

@@ -1,4 +1,5 @@
import os
import re
import json
import shutil
import subprocess
@@ -8,10 +9,10 @@ import struct
import threading
import concurrent.futures
import tempfile
import math
import yaml
from functools import partial
from typing import List, Tuple, Dict, Optional
from picard import config, log
from picard.ui.itemviews import (
BaseAction,
@@ -22,10 +23,7 @@ from picard.track import Track
from picard.album import Album
from picard.ui.options import OptionsPage, register_options_page
from picard.util import thread
from picard.coverart.image import (
CoverArtImage,
CoverArtImageError,
)
from picard.coverart.image import CoverArtImage
from PyQt5 import QtWidgets, QtCore
_analysis_semaphore = None
@@ -56,20 +54,23 @@ class AcousticBrainzNG:
binary_path += '.exe'
return binary_path
def _get_binary_paths(self) -> Tuple[str, str]:
def _get_binary_paths(self) -> Tuple[str, str, str]:
binaries_path = config.setting["acousticbrainz_ng_binaries_path"]
if not binaries_path:
raise ValueError("Binaries path not configured")
musicnn_binary_path = self._get_binary_path("streaming_musicnn_predict", binaries_path)
gaia_binary_path = self._get_binary_path("streaming_extractor_music", binaries_path)
rhythm_binary_path = self._get_binary_path("streaming_rhythmextractor_multifeature", binaries_path)
key_binary_path = self._get_binary_path("streaming_key", binaries_path)
if not os.path.exists(musicnn_binary_path):
raise FileNotFoundError(f"Binary {musicnn_binary_path} not found")
if not os.path.exists(gaia_binary_path):
raise FileNotFoundError(f"Binary {gaia_binary_path} not found")
return musicnn_binary_path, gaia_binary_path
if not os.path.exists(rhythm_binary_path):
raise FileNotFoundError(f"Binary {rhythm_binary_path} not found")
if not os.path.exists(key_binary_path):
raise FileNotFoundError(f"Binary {key_binary_path} not found")
return musicnn_binary_path, rhythm_binary_path, key_binary_path
def _run_musicnn_models(self, models: List[Tuple[str, str]], musicnn_binary_path: str, file: str, output_path: str) -> bool:
models_path = config.setting["acousticbrainz_ng_models_path"]
@@ -133,7 +134,7 @@ class AcousticBrainzNG:
return False
try:
musicnn_binary_path, gaia_binary_path = self._get_binary_paths()
musicnn_binary_path, rhythm_binary_path, key_binary_path = self._get_binary_paths()
except (ValueError, FileNotFoundError) as e:
log.error(str(e))
return False
@@ -147,80 +148,86 @@ class AcousticBrainzNG:
log.error(f"Error generating cache folder: {e}")
return False
gaia_success = True
def run_gaia():
nonlocal gaia_success
if os.path.exists(os.path.join(output_path, "gaia.json")):
return
jq_path = config.setting["acousticbrainz_ng_jq_path"]
if not jq_path or not os.path.exists(jq_path):
log.error("jq binary path not configured or invalid")
gaia_success = False
rhythm_success = True
def run_rhythm():
nonlocal rhythm_success
if os.path.exists(os.path.join(output_path, "rhythm.yaml")):
return
gaia_proc = subprocess.run(
[gaia_binary_path, file, "-"],
rhythm_proc = subprocess.run(
[rhythm_binary_path, file],
capture_output=True,
text=True,
env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if gaia_proc.returncode != 0:
gaia_success = False
log.error(f"Gaia binary {gaia_binary_path} failed on file {file} with exit code {gaia_proc.returncode}")
if gaia_proc.stdout:
log.error(f"Gaia stdout: {gaia_proc.stdout}")
if gaia_proc.stderr:
log.error(f"Gaia stderr: {gaia_proc.stderr}")
return
jq_filter = (
"{ rhythm: { bpm: .rhythm.bpm }, "
"tonal: { "
"chords_changes_rate: .tonal.chords_changes_rate, "
"chords_key: .tonal.chords_key, "
"chords_scale: .tonal.chords_scale, "
"key_temperley: { key: .tonal.key_temperley.key, scale: .tonal.key_temperley.scale, strength: .tonal.key_temperley.strength }, "
"key_krumhansl: { key: .tonal.key_krumhansl.key, scale: .tonal.key_krumhansl.scale, strength: .tonal.key_krumhansl.strength }, "
"key_edma: { key: .tonal.key_edma.key, scale: .tonal.key_edma.scale, strength: .tonal.key_edma.strength } "
"} }"
)
jq_proc = subprocess.run(
[jq_path, jq_filter],
input=gaia_proc.stdout,
capture_output=True,
text=True,
env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if jq_proc.returncode != 0:
gaia_success = False
log.error(f"jq failed to post-process Gaia JSON with exit code {jq_proc.returncode}")
if jq_proc.stdout:
log.error(f"jq stdout: {jq_proc.stdout}")
if jq_proc.stderr:
log.error(f"jq stderr: {jq_proc.stderr}")
if rhythm_proc.returncode != 0:
rhythm_success = False
log.error(f"Rhythm binary {rhythm_binary_path} failed on file {file} with exit code {rhythm_proc.returncode}")
if rhythm_proc.stdout:
log.error(f"Rhythm stdout: {rhythm_proc.stdout}")
if rhythm_proc.stderr:
log.error(f"Rhythm stderr: {rhythm_proc.stderr}")
return
try:
os.makedirs(output_path, exist_ok=True)
with open(os.path.join(output_path, "gaia.json"), "w", encoding="utf-8") as f:
f.write(jq_proc.stdout)
stdout = rhythm_proc.stdout or ""
lines = stdout.splitlines(keepends=True)
if not lines:
raise ValueError("Rhythm binary produced no stdout")
yaml_lines = lines[-5:] if len(lines) >= 5 else lines
yaml_str = "".join(yaml_lines)
if not yaml_str.strip():
raise ValueError("Empty YAML section extracted from rhythm binary output")
out_file = os.path.join(output_path, "rhythm.yaml")
with open(out_file, "w", encoding="utf-8") as f:
f.write(yaml_str)
except Exception as e:
gaia_success = False
log.error(f"Failed to write processed Gaia JSON: {e}")
rhythm_success = False
log.error(f"Failed to extract/save rhythm.yaml from rhythm binary stdout: {e}")
if rhythm_proc.stdout:
log.error(f"Rhythm stdout: {rhythm_proc.stdout}")
if rhythm_proc.stderr:
log.error(f"Rhythm stderr: {rhythm_proc.stderr}")
return
gaia_thread = threading.Thread(target=run_gaia)
gaia_thread.start()
key_success = True
def run_key():
nonlocal key_success
if os.path.exists(os.path.join(output_path, "key.yaml")):
return
key_proc = subprocess.run(
[key_binary_path, file, os.path.join(output_path, "key.yaml")],
capture_output=True,
text=True,
env=ENV,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if key_proc.returncode != 0:
key_success = False
log.error(f"Key binary {key_binary_path} failed on file {file} with exit code {key_proc.returncode}")
if key_proc.stdout:
log.error(f"Key stdout: {key_proc.stdout}")
if key_proc.stderr:
log.error(f"Key stderr: {key_proc.stderr}")
return
rhythm_thread = threading.Thread(target=run_rhythm)
rhythm_thread.start()
key_thread = threading.Thread(target=run_key)
key_thread.start()
musicnn_success = self._run_musicnn_models(REQUIRED_MODELS, musicnn_binary_path, file, output_path)
gaia_thread.join()
return gaia_success and musicnn_success
rhythm_thread.join()
key_thread.join()
return rhythm_success and key_success and musicnn_success
def analyze_optional(self, metadata: Dict, file: str) -> bool:
if not self._check_binaries():
@@ -232,7 +239,7 @@ class AcousticBrainzNG:
return False
try:
musicnn_binary_path, _ = self._get_binary_paths()
musicnn_binary_path = self._get_binary_paths()[0]
except (ValueError, FileNotFoundError) as e:
log.error(str(e))
return False
@@ -329,50 +336,48 @@ class AcousticBrainzNG:
metadata['mood'] = moods
metadata['tags'] = tags
gaia_data = {}
gaia_json_path = os.path.join(output_path, "gaia.json")
rhythm_data = {}
rhythm_yaml_path = os.path.join(output_path, "rhythm.yaml")
if os.path.exists(gaia_json_path):
try:
with open(gaia_json_path, 'r', encoding='utf-8') as f:
gaia_data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
log.error(f"Error reading Gaia JSON file: {e}")
key_data = {}
key_yaml_path = os.path.join(output_path, "key.yaml")
if os.path.exists(rhythm_yaml_path):
with open(rhythm_yaml_path, 'r', encoding='utf-8') as f:
loaded = yaml.safe_load(f)
if not isinstance(loaded, dict):
log.error("Invalid rhythm YAML format: expected a mapping at the top level")
return False
rhythm_data = loaded
else:
log.error(f"Gaia JSON file not found: {gaia_json_path}")
log.error(f"Rhythm YAML file not found: {rhythm_yaml_path}")
return False
if os.path.exists(key_yaml_path):
with open(key_yaml_path, 'r', encoding='utf-8') as f:
loaded = yaml.safe_load(f)
if not isinstance(loaded, dict):
log.error("Invalid key YAML format: expected a mapping at the top level")
return False
key_data = loaded
else:
log.error(f"Key YAML file not found: {key_yaml_path}")
return False
try:
metadata["bpm"] = int(round(gaia_data["rhythm"]["bpm"]))
metadata["bpm"] = int(round(rhythm_data["bpm"]))
metadata["key"] = "o" if key_data["tonal"]["key_scale"] == "off" else f"{key_data['tonal']['key']}{'m' if key_data['tonal']['key_scale'] == 'minor' else ''}"
if config.setting["acousticbrainz_ng_save_raw"]:
metadata["ab:lo:tonal:chords_changes_rate"] = gaia_data["tonal"]["chords_changes_rate"]
metadata["ab:lo:tonal:chords_key"] = gaia_data["tonal"]["chords_key"]
metadata["ab:lo:tonal:chords_scale"] = gaia_data["tonal"]["chords_scale"]
highestStrength = -1
selectedAlgorithm = None
for algorithm in GAIA_KEY_ALGORITHMS:
key_data = gaia_data["tonal"][f"key_{algorithm}"]
if key_data["strength"] > highestStrength:
highestStrength = key_data["strength"]
selectedAlgorithm = algorithm
if selectedAlgorithm:
selected_key_data = gaia_data["tonal"][f"key_{selectedAlgorithm}"]
metadata["key"] = "o" if selected_key_data["scale"] == "off" else f"{selected_key_data['key']}{'m' if selected_key_data['scale'] == 'minor' else ''}"
if config.setting["acousticbrainz_ng_save_raw"]:
metadata["ab:lo:tonal:key_scale"] = selected_key_data["scale"]
metadata["ab:lo:tonal:key_key"] = selected_key_data["key"]
metadata["ab:lo:tonal:key_scale"] = key_data["tonal"]["key_scale"]
metadata["ab:lo:tonal:key_key"] = key_data["tonal"]["key"]
return True
except Exception as e:
log.error(f"Error processing gaia data: {e}")
log.error(f"Error processing feature data: {e}")
return False
def parse_optional(self, metadata: Dict, file: str) -> bool:
@@ -671,7 +676,7 @@ class AcousticBrainzNG:
if not ffmpeg_path:
raise ValueError("FFmpeg path not configured")
replaygain_lufs_result = subprocess.run(
replaygain_proc = subprocess.run(
[ffmpeg_path, "-hide_banner", "-i", file_path, "-af", f"loudnorm=I={config.setting['acousticbrainz_ng_replaygain_reference_loudness']}:print_format=json", "-f", "null", "-"],
capture_output=True,
text=True,
@@ -679,40 +684,71 @@ class AcousticBrainzNG:
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if replaygain_lufs_result.returncode != 0:
log.error(f"FFmpeg failed for ReplayGain LUFS calculation on file {file_path} with exit code {replaygain_lufs_result.returncode}")
if replaygain_lufs_result.stdout:
log.error(f"FFmpeg stdout: {replaygain_lufs_result.stdout}")
if replaygain_lufs_result.stderr:
log.error(f"FFmpeg stderr: {replaygain_lufs_result.stderr}")
if replaygain_proc.returncode != 0:
log.error(f"FFmpeg failed for ReplayGain LUFS calculation on file {file_path} with exit code {replaygain_proc.returncode}")
if replaygain_proc.stdout:
log.error(f"FFmpeg stdout: {replaygain_proc.stdout}")
if replaygain_proc.stderr:
log.error(f"FFmpeg stderr: {replaygain_proc.stderr}")
return {}
replaygain_log = replaygain_proc.stderr or replaygain_proc.stdout
replaygain_log = "\n".join((replaygain_log or "").splitlines()[-15:])
replaygain_match = re.search(r'\{.*?\}', replaygain_log, re.S)
replaygain_matches = re.findall(r'\{.*?\}', replaygain_log, re.S) if not replaygain_match else None
replaygain_json_text = replaygain_match.group(0) if replaygain_match else (replaygain_matches[0] if replaygain_matches else None)
replaygain_gain = None
replaygain_peak = None
replaygain_range = None
try:
json_start = replaygain_lufs_result.stderr.find('{')
if json_start != -1:
json_str = replaygain_lufs_result.stderr[json_start:]
json_end = json_str.find('}') + 1
if json_end > 0:
loudnorm_data = json.loads(json_str[:json_end])
input_i = loudnorm_data.get('input_i')
input_tp = loudnorm_data.get('input_tp')
input_lra = loudnorm_data.get('input_lra')
if input_i and input_i != "-inf":
replaygain_gain = f"{(config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18) - float(input_i):.2f}"
replaygain_lufs_result: dict | None = None
if replaygain_json_text:
try:
replaygain_lufs_result = json.loads(replaygain_json_text)
except json.JSONDecodeError:
if replaygain_matches:
try:
replaygain_lufs_result = json.loads(replaygain_matches[-1])
except Exception:
replaygain_lufs_result = None
if input_tp and input_tp != "-inf":
replaygain_peak = f"{10 ** (float(input_tp) / 20):.6f}"
if input_lra and input_lra != "-inf":
replaygain_range = f"{float(input_lra):.2f}"
except (json.JSONDecodeError, ValueError, TypeError):
pass
input_i = replaygain_lufs_result.get('input_i') if replaygain_lufs_result else None
input_tp = replaygain_lufs_result.get('input_tp') if replaygain_lufs_result else None
input_lra = replaygain_lufs_result.get('input_lra') if replaygain_lufs_result else None
input_i_val = None
input_tp_val = None
input_lra_val = None
try:
if input_i is not None:
input_i_val = float(input_i)
except (TypeError, ValueError):
input_i_val = None
try:
if input_tp is not None:
input_tp_val = float(input_tp)
except (TypeError, ValueError):
input_tp_val = None
try:
if input_lra is not None:
input_lra_val = float(input_lra)
except (TypeError, ValueError):
input_lra_val = None
if input_i_val is not None and math.isfinite(input_i_val):
replaygain_gain = f"{(config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18) - input_i_val:.2f}"
if input_tp_val is not None and math.isfinite(input_tp_val):
replaygain_peak = f"{10 ** (input_tp_val / 20):.6f}"
if input_lra_val is not None and math.isfinite(input_lra_val):
replaygain_range = f"{input_lra_val:.2f}"
result: Dict = {
"replaygain_track_gain": replaygain_gain,
@@ -721,7 +757,7 @@ class AcousticBrainzNG:
"replaygain_reference_loudness": f"{(config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18):.2f}"
}
r128_result = subprocess.run(
r128_proc = subprocess.run(
[ffmpeg_path, "-hide_banner", "-i", file_path, "-af", "loudnorm=I=-23:print_format=json", "-f", "null", "-"],
capture_output=True,
text=True,
@@ -729,36 +765,52 @@ class AcousticBrainzNG:
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if r128_result.returncode != 0:
log.error(f"FFmpeg failed for R128 calculation on file {file_path} with exit code {r128_result.returncode}")
if r128_result.stdout:
log.error(f"FFmpeg stdout: {r128_result.stdout}")
if r128_result.stderr:
log.error(f"FFmpeg stderr: {r128_result.stderr}")
if r128_proc.returncode != 0:
log.error(f"FFmpeg failed for R128 calculation on file {file_path} with exit code {r128_proc.returncode}")
if r128_proc.stdout:
log.error(f"FFmpeg stdout: {r128_proc.stdout}")
if r128_proc.stderr:
log.error(f"FFmpeg stderr: {r128_proc.stderr}")
return result
r128_log = r128_proc.stderr or r128_proc.stdout
r128_log = "\n".join((r128_log or "").splitlines()[-15:])
r128_match = re.search(r'\{.*?\}', r128_log, re.S)
r128_matches = re.findall(r'\{.*?\}', r128_log, re.S) if not r128_match else None
r128_json_text = r128_match.group(0) if r128_match else (r128_matches[0] if r128_matches else None)
r128_track_gain = None
r128_data: dict | None = None
if r128_json_text:
try:
r128_data = json.loads(r128_json_text)
except json.JSONDecodeError:
if r128_matches:
try:
r128_data = json.loads(r128_matches[-1])
except Exception:
r128_data = None
r128_input_i = r128_data.get('input_i') if r128_data else None
r128_input_i_val = None
try:
json_start = r128_result.stderr.find('{')
if json_start != -1:
json_str = r128_result.stderr[json_start:]
json_end = json_str.find('}') + 1
if json_end > 0:
r128_data = json.loads(json_str[:json_end])
r128_input_i = r128_data.get('input_i')
if r128_input_i and r128_input_i != "-inf":
r128_gain_db = -23 - float(r128_input_i)
r128_track_gain = int(round(r128_gain_db * 256))
if r128_track_gain < -32768:
r128_track_gain = -32768
elif r128_track_gain > 32767:
r128_track_gain = 32767
except (json.JSONDecodeError, ValueError, TypeError):
pass
if r128_input_i is not None:
r128_input_i_val = int(r128_input_i)
except (TypeError, ValueError):
r128_input_i_val = None
if r128_input_i_val is not None and math.isfinite(r128_input_i_val):
r128_gain_db = -23 - r128_input_i_val
r128_track_gain = int(round(r128_gain_db * 256))
if r128_track_gain < -32768:
r128_track_gain = -32768
elif r128_track_gain > 32767:
r128_track_gain = 32767
result["r128_track_gain"] = r128_track_gain
@@ -783,11 +835,12 @@ class AcousticBrainzNG:
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as concat_file:
for audio_file in album_track_files:
concat_file.write(f"file '{audio_file}'\n")
escaped_file = audio_file.replace("'", "'\\''")
concat_file.write(f"file '{escaped_file}'\n")
concat_file_path = concat_file.name
try:
album_replaygain_result = subprocess.run(
album_replaygain_proc = subprocess.run(
[ffmpeg_path, "-hide_banner", "-f", "concat", "-safe", "0", "-i", concat_file_path,
"-vn", "-af", f"loudnorm=I={config.setting['acousticbrainz_ng_replaygain_reference_loudness']}:print_format=json", "-f", "null", "-"],
capture_output=True,
@@ -796,49 +849,75 @@ class AcousticBrainzNG:
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if album_replaygain_result.returncode != 0:
log.error(f"FFmpeg failed for album ReplayGain calculation on {len(album_track_files)} files with exit code {album_replaygain_result.returncode}")
if album_replaygain_proc.returncode != 0:
log.error(f"FFmpeg failed for album ReplayGain calculation on {len(album_track_files)} files with exit code {album_replaygain_proc.returncode}")
log.error(f"Album files: {', '.join(album_track_files)}")
if album_replaygain_result.stdout:
log.error(f"FFmpeg stdout: {album_replaygain_result.stdout}")
if album_replaygain_result.stderr:
log.error(f"FFmpeg stderr: {album_replaygain_result.stderr}")
if album_replaygain_proc.stdout:
log.error(f"FFmpeg stdout: {album_replaygain_proc.stdout}")
if album_replaygain_proc.stderr:
log.error(f"FFmpeg stderr: {album_replaygain_proc.stderr}")
return {}
album_replaygain_log = album_replaygain_proc.stderr or album_replaygain_proc.stdout
album_replaygain_match = re.search(r'\{.*?\}', album_replaygain_log, re.S)
album_replaygain_matches = re.findall(r'\{.*?\}', album_replaygain_log, re.S) if not album_replaygain_match else None
album_replaygain_json_text = album_replaygain_match.group(0) if album_replaygain_match else (album_replaygain_matches[0] if album_replaygain_matches else None)
album_gain = None
album_peak = None
album_range = None
try:
json_start = album_replaygain_result.stderr.find('{')
if json_start != -1:
json_str = album_replaygain_result.stderr[json_start:]
json_end = json_str.find('}') + 1
if json_end > 0:
loudnorm_data = json.loads(json_str[:json_end])
input_i = loudnorm_data.get('input_i')
input_tp = loudnorm_data.get('input_tp')
input_lra = loudnorm_data.get('input_lra')
if input_i and input_i != "-inf":
album_gain = f"{(config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18) - float(input_i):.2f}"
if input_tp and input_tp != "-inf":
album_peak = f"{10 ** (float(input_tp) / 20):.6f}"
if input_lra and input_lra != "-inf":
album_range = f"{float(input_lra):.2f}"
except (json.JSONDecodeError, ValueError, TypeError):
pass
loudnorm_data: dict | None = None
if album_replaygain_json_text:
try:
loudnorm_data = json.loads(album_replaygain_json_text)
except json.JSONDecodeError:
if album_replaygain_matches:
try:
loudnorm_data = json.loads(album_replaygain_matches[-1])
except Exception:
loudnorm_data = None
input_i = loudnorm_data.get('input_i') if loudnorm_data else None
input_tp = loudnorm_data.get('input_tp') if loudnorm_data else None
input_lra = loudnorm_data.get('input_lra') if loudnorm_data else None
try:
if input_i:
input_i_val = float(input_i)
except (TypeError, ValueError):
input_i_val = None
try:
if input_tp:
input_tp_val = float(input_tp)
except (TypeError, ValueError):
input_tp_val = None
try:
if input_lra:
input_lra_val = float(input_lra)
except (TypeError, ValueError):
input_lra_val = None
if input_i_val is not None and math.isfinite(input_i_val):
album_gain = f"{(config.setting['acousticbrainz_ng_replaygain_reference_loudness'] or -18) - input_i_val:.2f}"
if input_tp_val is not None and math.isfinite(input_tp_val):
album_peak = f"{10 ** (input_tp_val / 20):.6f}"
if input_lra_val is not None and math.isfinite(input_lra_val):
album_range = f"{input_lra_val:.2f}"
result: Dict = {
"replaygain_album_gain": album_gain,
"replaygain_album_peak": album_peak,
"replaygain_album_range": album_range
}
album_r128_result = subprocess.run(
album_r128_proc = subprocess.run(
[ffmpeg_path, "-hide_banner", "-f", "concat", "-safe", "0", "-i", concat_file_path,
"-vn", "-af", "loudnorm=I=-23:print_format=json", "-f", "null", "-"],
capture_output=True,
@@ -847,37 +926,51 @@ class AcousticBrainzNG:
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
)
if album_r128_result.returncode != 0:
log.error(f"FFmpeg failed for album R128 calculation on {len(album_track_files)} files with exit code {album_r128_result.returncode}")
if album_r128_proc.returncode != 0:
log.error(f"FFmpeg failed for album R128 calculation on {len(album_track_files)} files with exit code {album_r128_proc.returncode}")
log.error(f"Album files: {', '.join(album_track_files)}")
if album_r128_result.stdout:
log.error(f"FFmpeg stdout: {album_r128_result.stdout}")
if album_r128_result.stderr:
log.error(f"FFmpeg stderr: {album_r128_result.stderr}")
if album_r128_proc.stdout:
log.error(f"FFmpeg stdout: {album_r128_proc.stdout}")
if album_r128_proc.stderr:
log.error(f"FFmpeg stderr: {album_r128_proc.stderr}")
return result
album_r128_log = album_r128_proc.stderr or album_r128_proc.stdout
album_r128_match = re.search(r'\{.*?\}', album_r128_log, re.S)
album_r128_matches = re.findall(r'\{.*?\}', album_r128_log, re.S) if not album_r128_match else None
album_r128_json_text = album_r128_match.group(0) if album_r128_match else (album_r128_matches[0] if album_r128_matches else None)
r128_album_gain = None
r128_data: dict | None = None
if album_r128_json_text:
try:
r128_data = json.loads(album_r128_json_text)
except json.JSONDecodeError:
if album_r128_matches:
try:
r128_data = json.loads(album_r128_matches[-1])
except Exception:
r128_data = None
r128_input_i = r128_data.get('input_i') if r128_data else None
try:
json_start = album_r128_result.stderr.find('{')
if json_start != -1:
json_str = album_r128_result.stderr[json_start:]
json_end = json_str.find('}') + 1
if json_end > 0:
r128_data = json.loads(json_str[:json_end])
r128_input_i = r128_data.get('input_i')
if r128_input_i and r128_input_i != "-inf":
r128_gain_db = -23 - float(r128_input_i)
r128_album_gain = int(round(r128_gain_db * 256))
if r128_album_gain < -32768:
r128_album_gain = -32768
elif r128_album_gain > 32767:
r128_album_gain = 32767
except (json.JSONDecodeError, ValueError, TypeError):
pass
if r128_input_i:
r128_input_i_val = int(r128_input_i)
except (TypeError, ValueError):
r128_input_i_val = None
if r128_input_i_val is not None and math.isfinite(r128_input_i_val):
r128_gain_db = -23 - r128_input_i_val
r128_album_gain = int(round(r128_gain_db * 256))
if r128_album_gain < -32768:
r128_album_gain = -32768
elif r128_album_gain > 32767:
r128_album_gain = 32767
result["r128_album_gain"] = r128_album_gain
@@ -1362,8 +1455,8 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
def update_concurrent_processes():
concurrent_analyses = self.concurrent_analyses_input.value()
musicnn_workers = self.musicnn_workers_input.value()
max_processes = concurrent_analyses + (concurrent_analyses * musicnn_workers)
breakdown = f"[{concurrent_analyses} gaia processes + ({concurrent_analyses} x {musicnn_workers}) MusicNN processes]"
max_processes = (2 * concurrent_analyses) + (concurrent_analyses * musicnn_workers)
breakdown = f"[(2 x {concurrent_analyses}) feature processes + ({concurrent_analyses} x {musicnn_workers}) MusicNN processes]"
self.concurrent_processes_display.setText(f"{breakdown} = <span style='font-weight: bold;'>{max_processes}</span>")
self.concurrent_analyses_input.valueChanged.connect(update_concurrent_processes)
@@ -1396,15 +1489,6 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
lambda: self._browse_file(self.ffmpeg_path_input),
lambda: (self._check_binaries(show_success=True), None)[1]
)
# jq path
self.jq_path_input = QtWidgets.QLineEdit(self)
self.jq_path_input.setPlaceholderText("Path to jq")
jq_layout = self._create_path_input_layout(
self.jq_path_input,
lambda: self._browse_file(self.jq_path_input),
lambda: (self._check_binaries(show_success=True), None)[1]
)
# Models path
self.models_path_input = QtWidgets.QLineEdit(self)
@@ -1425,8 +1509,6 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
paths_layout.addWidget(QtWidgets.QLabel("FFmpeg", self))
paths_layout.addLayout(ffmpeg_layout)
paths_layout.addWidget(QtWidgets.QLabel("jq", self))
paths_layout.addLayout(jq_layout)
paths_layout.addWidget(QtWidgets.QLabel("Binaries", self))
paths_layout.addLayout(binaries_layout)
paths_layout.addWidget(QtWidgets.QLabel("Models", self))
@@ -1453,11 +1535,6 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
if not ffmpeg_path or not os.path.exists(ffmpeg_path):
QtWidgets.QMessageBox.warning(self, "Binaries", "Invalid or empty FFmpeg path.")
return False
jq_path = self.jq_path_input.text()
if not jq_path or not os.path.exists(jq_path):
QtWidgets.QMessageBox.warning(self, "Binaries", "Invalid or empty jq path.")
return False
missing_binaries = []
for binary in REQUIRED_BINARIES:
@@ -1481,20 +1558,6 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
missing_binaries.append("FFmpeg (unable to execute)")
log.error(f"Exception running FFmpeg version check: {e}")
try:
result = subprocess.run([jq_path, "--version"], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
if result.returncode != 0 or not result.stdout.startswith("jq-"):
missing_binaries.append("jq (invalid executable)")
if result.returncode != 0:
log.error(f"jq version check failed with exit code {result.returncode}")
if result.stdout:
log.error(f"jq stdout: {result.stdout}")
if result.stderr:
log.error(f"jq stderr: {result.stderr}")
except Exception as e:
missing_binaries.append("jq (unable to execute)")
log.error(f"Exception running jq version check: {e}")
if missing_binaries:
message = f"Missing binaries:\n" + "\n".join(f"{binary}" for binary in missing_binaries)
QtWidgets.QMessageBox.warning(self, "Binaries", message)
@@ -1586,7 +1649,6 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
self.binaries_path_input.setText(config.setting["acousticbrainz_ng_binaries_path"])
self.ffmpeg_path_input.setText(config.setting["acousticbrainz_ng_ffmpeg_path"])
self.jq_path_input.setText(config.setting["acousticbrainz_ng_jq_path"])
self.models_path_input.setText(config.setting["acousticbrainz_ng_models_path"])
self.cache_path_input.setText(config.setting["acousticbrainz_ng_cache_path"])
@@ -1597,11 +1659,7 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
config.setting["acousticbrainz_ng_analyze_optional"] = self.analyze_optional_checkbox.isChecked()
config.setting["acousticbrainz_ng_save_raw"] = self.save_raw_checkbox.isChecked()
config.setting["acousticbrainz_ng_calculate_replaygain"] = self.calculate_replaygain_checkbox.isChecked()
if self.analyze_optional_checkbox.isChecked():
config.setting["acousticbrainz_ng_save_fingerprint"] = self.save_fingerprint_checkbox.isChecked()
else:
config.setting["acousticbrainz_ng_save_fingerprint"] = False
config.setting["acousticbrainz_ng_save_fingerprint"] = self.save_fingerprint_checkbox.isChecked()
max_workers = max(1, min(self.musicnn_workers_input.value(), max(len(REQUIRED_MODELS), len(OPTIONAL_MODELS))))
config.setting["acousticbrainz_ng_max_musicnn_workers"] = max_workers
@@ -1614,7 +1672,6 @@ class AcousticBrainzNGOptionsPage(OptionsPage):
config.setting["acousticbrainz_ng_binaries_path"] = self.binaries_path_input.text()
config.setting["acousticbrainz_ng_ffmpeg_path"] = self.ffmpeg_path_input.text()
config.setting["acousticbrainz_ng_jq_path"] = self.jq_path_input.text()
config.setting["acousticbrainz_ng_models_path"] = self.models_path_input.text()
config.setting["acousticbrainz_ng_cache_path"] = self.cache_path_input.text()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -18,7 +18,7 @@ External dependencies:
</ul>
<strong>This plugin is CPU heavy!</strong>
"""
PLUGIN_VERSION = "1.1.1"
PLUGIN_VERSION = "1.1.3"
PLUGIN_API_VERSIONS = ["2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13"]
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
@@ -46,8 +46,9 @@ OPTIONAL_MODELS: List[Tuple[str, str]] = [
]
REQUIRED_BINARIES: List[str] = [
"streaming_extractor_music",
"streaming_rhythmextractor_multifeature",
"streaming_musicnn_predict",
"streaming_key",
"streaming_md5",
]
@@ -57,7 +58,6 @@ ENV['TF_ENABLE_ONEDNN_OPTS'] = "0"
CONFIG_OPTIONS = [
TextOption("setting", "acousticbrainz_ng_binaries_path", os.path.join(os.path.dirname(__file__), "bin")),
TextOption("setting", "acousticbrainz_ng_ffmpeg_path", os.path.join(os.path.dirname(sys.executable), "ffmpeg" + (".exe" if os.name == "nt" else ""))),
TextOption("setting", "acousticbrainz_ng_jq_path", os.path.join(os.path.dirname(sys.executable), "jq" + (".exe" if os.name == "nt" else ""))),
TextOption("setting", "acousticbrainz_ng_models_path", os.path.join(os.path.dirname(__file__), "models")),
TextOption("setting", "acousticbrainz_ng_cache_path", os.path.join(os.path.dirname(__file__), "cache")),
IntOption("setting", "acousticbrainz_ng_max_musicnn_workers", 4),
@@ -66,7 +66,5 @@ CONFIG_OPTIONS = [
BoolOption("setting", "acousticbrainz_ng_analyze_optional", False),
BoolOption("setting", "acousticbrainz_ng_save_raw", False),
BoolOption("setting", "acousticbrainz_ng_calculate_replaygain", True),
BoolOption("setting", "acousticbrainz_ng_save_fingerprint", True)
]
GAIA_KEY_ALGORITHMS = ["edma", "krumhansl", "temperley"]
BoolOption("setting", "acousticbrainz_ng_save_fingerprint", False)
]

1
misc/llms/README.md Normal file
View File

@@ -0,0 +1 @@
In this folder are some prompts you can use to generate smart playlists that follow a theme

351
misc/llms/navidrome.md Normal file
View File

@@ -0,0 +1,351 @@
You are an expert Navidrome DJ who designs precise Smart Playlists. Your job is to turn a vibe or requirement into a clean, minimal set of rules that work in Navidromes Smart Playlist builder.
# Navidrome Smart Playlist documentation:
Smart Playlists in Navidrome offer a dynamic way to organize and enjoy your music collection. They are created using JSON objects stored in files with a `.nsp` extension. These playlists are automatically updated based on specified criteria, providing a personalized and evolving music experience.
## Creating Smart Playlists
To create a Smart Playlist, you need to define a JSON object with specific fields and operators that describe the criteria for selecting tracks. The JSON object is stored in a `.nsp` file
Here are some examples:
### Example 1: Recently Played Tracks
This playlist includes tracks played in the last 30 days, sorted by the most recently played.
```json
{
"name": "Recently Played",
"comment": "Recently played tracks",
"all": [{ "inTheLast": { "lastPlayed": 30 } }],
"sort": "lastPlayed",
"order": "desc",
"limit": 100
}
```
## Example 2: 80's Top Songs
This playlist features top-rated songs from the 1980s.
```json
{
"all": [
{ "any": [{ "is": { "loved": true } }, { "gt": { "rating": 3 } }] },
{ "inTheRange": { "year": [1981, 1990] } }
],
"sort": "year",
"order": "desc",
"limit": 25
}
```
### Example 3: Favourites
This playlist includes all loved tracks, sorted by the date they were loved.
```json
{
"all": [{ "is": { "loved": true } }],
"sort": "dateLoved",
"order": "desc",
"limit": 500
}
```
### Example 4: All Songs in Random Order
This playlist includes all songs in a random order. Note: This is not recommended for large libraries.
```json
{
"all": [{ "gt": { "playCount": -1 } }],
"sort": "random"
// limit: 1000 // Uncomment this line to limit the number of songs
}
```
### Example 5: Multi-Field Sorting
This playlist demonstrates multiple sort fields - songs from the 2000s, sorted by year (descending), then by rating (descending), then by title (ascending).
```json
{
"name": "2000s Hits by Year and Rating",
"all": [{ "inTheRange": { "year": [2000, 2009] } }],
"sort": "-year,-rating,title",
"limit": 200
}
```
### Example 6: Library-Specific Playlist
This playlist includes only high-rated songs from a specific library (useful in multi-library setups).
```json
{
"name": "High-Rated Songs from Library 2",
"all": [{ "is": { "library_id": 2 } }, { "gt": { "rating": 4 } }],
"sort": "-rating,title",
"limit": 100
}
```
## Troubleshooting Common Issues
### Special Characters in Conditions
If you encounter issues with conditions like `contains` or `endsWith`, especially with special characters like
underscores (`_`), be aware that these might be ignored in some computations. Adjust your conditions accordingly.
### Sorting by multiple fields
You can sort by multiple fields by separating them with commas. You can also control the sort direction for each field by prefixing it with `+` (ascending, default) or `-` (descending).
Examples:
- `"sort": "year,title"` - Sort by year first (ascending), then by title (ascending)
- `"sort": "year,-rating"` - Sort by year (ascending), then by rating (descending)
- `"sort": "-lastplayed,title"` - Sort by last played date (descending), then by title (ascending)
The global `order` field can still be used and will reverse the direction of all sort fields.
## Additional Resources
### Fields
Here's a table of fields you can use in your Smart Playlists:
| Field | Description |
| ---------------------- | ---------------------------------------- |
| `title` | Track title |
| `album` | Album name |
| `hascoverart` | Track has cover art |
| `tracknumber` | Track number |
| `discnumber` | Disc number |
| `year` | Year of release |
| `date` | Recording date |
| `originalyear` | Original year |
| `originaldate` | Original date |
| `releaseyear` | Release year |
| `releasedate` | Release date |
| `size` | File size |
| `compilation` | Compilation album |
| `dateadded` | Date added to library |
| `datemodified` | Date modified |
| `discsubtitle` | Disc subtitle |
| `comment` | Comment |
| `lyrics` | Lyrics |
| `sorttitle` | Sorted track title |
| `sortalbum` | Sorted album name |
| `sortartist` | Sorted artist name |
| `sortalbumartist` | Sorted album artist name |
| `albumtype` | Album type |
| `albumcomment` | Album comment |
| `catalognumber` | Catalog number |
| `filepath` | File path, relative to the MusicFolder |
| `filetype` | File type |
| `duration` | Track duration |
| `bitrate` | Bitrate |
| `bitdepth` | Bit depth |
| `bpm` | Beats per minute |
| `channels` | Audio channels |
| `loved` | Track is loved |
| `dateloved` | Date track was loved |
| `lastplayed` | Date track was last played |
| `playcount` | Number of times track was played |
| `rating` | Track rating |
| `mbz_album_id` | MusicBrainz Album ID |
| `mbz_album_artist_id` | MusicBrainz Album Artist ID |
| `mbz_artist_id` | MusicBrainz Artist ID |
| `mbz_recording_id` | MusicBrainz Recording ID |
| `mbz_release_track_id` | MusicBrainz Release Track ID |
| `mbz_release_group_id` | MusicBrainz Release Group ID |
| `library_id` | Library ID (for multi-library filtering) |
##### Notes
- Dates must be in the format `"YYYY-MM-DD"`.
- Booleans must not be enclosed in quotes. Example: `{ "is": { "loved": true } }`.
- `filepath` is relative to the music library folder. Ensure paths are correctly specified without the `/music` prefix (or whatever value you set in `MusicFolder`).
- Numeric fields like `library_id`, `year`, `tracknumber`, `discnumber`, `size`, `duration`, `bitrate`, `bitdepth`, `bpm`, `channels`, `playcount`, and `rating` support numeric comparisons (`gt`, `lt`, `inTheRange`, etc.).
##### Special Fields
- `random`: Used for random sorting (e.g., `"sort": "random"`)
- `value`: Used internally for tag and role-based queries
##### MusicBrainz Fields
The following fields contain MusicBrainz IDs that can be used to create playlists based on specific MusicBrainz entities:
- `mbz_album_id`: Filter by specific MusicBrainz album
- `mbz_album_artist_id`: Filter by specific MusicBrainz album artist
- `mbz_artist_id`: Filter by specific MusicBrainz artist
- `mbz_recording_id`: Filter by specific MusicBrainz recording
- `mbz_release_track_id`: Filter by specific MusicBrainz release track
- `mbz_release_group_id`: Filter by specific MusicBrainz release group
##### Additional Fields
You can also use these fields in your Smart Playlists, they are highly recommended as they are generated using sonic analysis on each song:
| Field | Description | Type |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| acoustic | Classification by type of sound (acoustic) | A float between 0 and 1, 1 being the most confident |
| notacoustic | Classification by type of sound (not acoustic) | A float between 0 and 1, 1 being the most confident |
| aggressive | Classification by mood (aggressive) | A float between 0 and 1, 1 being the most confident |
| notaggressive | Classification by mood (not aggressive) | A float between 0 and 1, 1 being the most confident |
| electronic | Classification by mood (electronic) | A float between 0 and 1, 1 being the most confident |
| notelectronic | Classification by mood (not electronic) | A float between 0 and 1, 1 being the most confident |
| happy | Classification by mood (happy) | A float between 0 and 1, 1 being the most confident |
| nothappy | Classification by mood (not happy) | A float between 0 and 1, 1 being the most confident |
| party | Classification by mood (party) | A float between 0 and 1, 1 being the most confident |
| notparty | Classification by mood (not party) | A float between 0 and 1, 1 being the most confident |
| relaxed | Classification by mood (relaxed) | A float between 0 and 1, 1 being the most confident |
| notrelaxed | Classification by mood (not relaxed) | A float between 0 and 1, 1 being the most confident |
| sad | Classification by mood (sad) | A float between 0 and 1, 1 being the most confident |
| notsad | Classification by mood (not sad) | A float between 0 and 1, 1 being the most confident |
| danceable | Classification by mood (danceable) | A float between 0 and 1, 1 being the most confident |
| notdanceable | Classification by mood (not danceable) | A float between 0 and 1, 1 being the most confident |
| female | Classification of vocal music by gender (female) | A float between 0 and 1, 1 being the most confident |
| male | Classification of vocal music by gender (male) | A float between 0 and 1, 1 being the most confident |
| atonal | Classification by tonality (atonal) | A float between 0 and 1, 1 being the most confident |
| tonal | Classification by tonality (tonal) | A float between 0 and 1, 1 being the most confident |
| instrumental | Classification of music with voice | A float between 0 and 1, 1 being the most confident |
| voice | Classification of instrumental music | A float between 0 and 1, 1 being the most confident |
| mood | A culmination of the 11 pairs of values above, each value is the value of the pair that has the higher confidence. Note: a song can be both (not) happy and (not) sad at the same time | An array of strings. has length 11, possible combinations (separated by a slash) are: Acoustic/Not acoustic, Aggressive/Not agressive, Electronic/Not electronic, Happy/Not happy, Party/Not party, Sad/Not sad, Danceable/Not danceable, Male/Female, Atonal/Tonal, Voice/Instrumental |
#### Even more fields
Additionally, more genre-related (although, it may be better to use the official genre field instead of the genre-related fields below, additionally, you cannot assume the user's music library has every genre available) and technical fields are available:
| Field | Description | Type |
| -------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| ambient | Ambient genre | A float between 0 and 1, 1 being the most confident |
| drumandbass | Drum & bass genre | A float between 0 and 1, 1 being the most confident |
| house | House genre | A float between 0 and 1, 1 being the most confident |
| techno | Techno genre | A float between 0 and 1, 1 being the most confident |
| trance | Trance genre | A float between 0 and 1, 1 being the most confident |
| classical_rosamerica | Classical genre | A float between 0 and 1, 1 being the most confident |
| dance | Dance genre | A float between 0 and 1, 1 being the most confident |
| hiphop_rosamerica | Hip hop genre | A float between 0 and 1, 1 being the most confident |
| jazz_rosamerica | Jazz genre | A float between 0 and 1, 1 being the most confident |
| pop_rosamerica | Pop genre | A float between 0 and 1, 1 being the most confident |
| rhythmandblues | Rhythm & blues genre | A float between 0 and 1, 1 being the most confident |
| rock_rosamerica | Rock genre | A float between 0 and 1, 1 being the most confident |
| speech | Speech genre | A float between 0 and 1, 1 being the most confident |
| blues | Blues genre | A float between 0 and 1, 1 being the most confident |
| classical_tzanetakis | Classical genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| country | Country genre | A float between 0 and 1, 1 being the most confident |
| disco | Disco genre | A float between 0 and 1, 1 being the most confident |
| hiphop_tzanetakis | Hip hop genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| jazz_tzanetakis | Jazz genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| metal | Metal genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| pop_tzanetakis | Pop genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| reggae | Reggae genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| rock_tzanetakis | Rock genre (from the GTZAN Genre Collection) | A float between 0 and 1, 1 being the most confident |
| tags | Top 5 MSD tags | An array of strings, has length 5, consists of the top 5 most confident Million Songs Database tags (see the table below this one) |
| chordschangesrate | Chords change rate | float |
| chordskey | Chords key | string |
| chordscale | Chords scale | string |
| keykey | Key | string |
| keyscale | Key scale | string |
| key | Key and key scale | string |
##### MSD Fields
Additionally, more fields are available, each field in the following table represents a tag from the top 50 tags in the Million Song Dataset, each value is a float between 0 and 1, 1 being the most confident (also, similar to the table above, do not assume the user has a wide variety of music):
| Field | Tag |
| ------------------ | ---------------- |
| msd00s | 00s |
| msd60s | 60s |
| msd70s | 70s |
| msd80s | 80s |
| msd90s | 90s |
| msdacoustic | acoustic |
| msdalternative | alternative |
| msdalternativerock | alternative rock |
| msdambient | ambient |
| msdbeautiful | beautiful |
| msdblues | blues |
| msdcatchy | catchy |
| msdchill | chill |
| msdchillout | chillout |
| msdclassicrock | classic rock |
| msdcountry | country |
| msddance | dance |
| msdeasylistening | easy listening |
| msdelectro | electro |
| msdelectronic | electronic |
| msdelectronica | electronica |
| msdexperimental | experimental |
| msdfemalevocalist | female vocalist |
| msdfemalevocalists | female vocalists |
| msdfolk | folk |
| msdfunk | funk |
| msdguitar | guitar |
| msdhappy | happy |
| msdhardrock | hard rock |
| msdheavymetal | heavy metal |
| msdhiphop | hip-hop |
| msdhouse | house |
| msdindie | indie |
| msdindiepop | indie pop |
| msdindierock | indie rock |
| msdinstrumental | instrumental |
| msdjazz | jazz |
| msdmalevocalists | male vocalists |
| msdmellow | mellow |
| msdmetal | metal |
| msdoldies | oldies |
| msdparty | party |
| msdpop | pop |
| msdprogressiverock | progressive rock |
| msdpunk | punk |
| msdrnb | rnb |
| msdrock | rock |
| msdsad | sad |
| msdsexy | sexy |
| msdsoul | soul |
### Operators
Here's a table of operators you can use in your Smart Playlists:
| Operator | Description | Argument type |
| ------------- | ------------------------ | ----------------------------- |
| is | Equal | String, Number, Boolean |
| isNot | Not equal | String, Number, Boolean |
| gt | Greater than | Number |
| lt | Less than | Number |
| contains | Contains | String |
| notContains | Does not contain | String |
| startsWith | Starts with | String |
| endsWith | Ends with | String |
| inTheRange | In the range (inclusive) | Array of two numbers or dates |
| before | Before | Date ("YYYY-MM-DD") |
| after | After | Date ("YYYY-MM-DD") |
| inTheLast | In the last | Number of days |
| notInTheLast | Not in the last | Number of days |
| inPlaylist | In playlist | Another playlist's ID |
| notInPlaylist | Not in playlist | Another playlist's ID |
The nature of the field determines the argument type. For example, `year` and `tracknumber` require a number, while `title` and `album` require a string.
# Process:
1. Ask up to 3 quick clarifying questions only if necessary
2. If info is sufficient, produce the final playlist json.
# Output format (strict):
Output the generated smart playlist rules json as well as a short and simple justification for each rule below. Ensure the playlist has a name and description.
# Guidance:
- Keep it concise; avoid prose outside the specified fields.
- Feel free to chain and nest `all`'s and `any`'s (series of ANDs and ORs respectively), but remember to keep the playlist rules minimal to avoid an empty playlist
# Input:
{enter your desired playlist here}

404
misc/navidrome.toml Normal file
View File

@@ -0,0 +1,404 @@
Tags.tags.Aliases = ["tags", "TXXX:tags"]
Tags.tags.Type = "string"
Tags.tags.Split = [";", "/", ","]
Tags.tags.Album = false
Tags.danceable.Aliases = ["ab:hi:danceability:danceable"]
Tags.danceable.Type = "float"
Tags.danceable.Album = false
Tags.notdanceable.Aliases = ["ab:hi:danceability:not danceable"]
Tags.notdanceable.Type = "float"
Tags.notdanceable.Album = false
Tags.female.Aliases = ["ab:hi:gender:female"]
Tags.female.Type = "float"
Tags.female.Album = false
Tags.male.Aliases = ["ab:hi:gender:male"]
Tags.male.Type = "float"
Tags.male.Album = false
Tags.ambient.Aliases = ["ab:hi:genre_electronic:ambient"]
Tags.ambient.Type = "float"
Tags.ambient.Album = false
Tags.drumandbass.Aliases = ["ab:hi:genre_electronic:drum and bass"]
Tags.drumandbass.Type = "float"
Tags.drumandbass.Album = false
Tags.house.Aliases = ["ab:hi:genre_electronic:house"]
Tags.house.Type = "float"
Tags.house.Album = false
Tags.techno.Aliases = ["ab:hi:genre_electronic:techno"]
Tags.techno.Type = "float"
Tags.techno.Album = false
Tags.trance.Aliases = ["ab:hi:genre_electronic:trance"]
Tags.trance.Type = "float"
Tags.trance.Album = false
Tags.classical_rosamerica.Aliases = ["ab:hi:genre_rosamerica:classical"]
Tags.classical_rosamerica.Type = "float"
Tags.classical_rosamerica.Album = false
Tags.dance.Aliases = ["ab:hi:genre_rosamerica:dance"]
Tags.dance.Type = "float"
Tags.dance.Album = false
Tags.hiphop_rosamerica.Aliases = ["ab:hi:genre_rosamerica:hiphop"]
Tags.hiphop_rosamerica.Type = "float"
Tags.hiphop_rosamerica.Album = false
Tags.jazz_rosamerica.Aliases = ["ab:hi:genre_rosamerica:jazz"]
Tags.jazz_rosamerica.Type = "float"
Tags.jazz_rosamerica.Album = false
Tags.pop_rosamerica.Aliases = ["ab:hi:genre_rosamerica:pop"]
Tags.pop_rosamerica.Type = "float"
Tags.pop_rosamerica.Album = false
Tags.rhythmandblues.Aliases = ["ab:hi:genre_rosamerica:rhythm and blues"]
Tags.rhythmandblues.Type = "float"
Tags.rhythmandblues.Album = false
Tags.rock_rosamerica.Aliases = ["ab:hi:genre_rosamerica:rock"]
Tags.rock_rosamerica.Type = "float"
Tags.rock_rosamerica.Album = false
Tags.speech.Aliases = ["ab:hi:genre_rosamerica:speech"]
Tags.speech.Type = "float"
Tags.speech.Album = false
Tags.blues.Aliases = ["ab:hi:genre_tzanetakis:blues"]
Tags.blues.Type = "float"
Tags.blues.Album = false
Tags.classical_tzanetakis.Aliases = ["ab:hi:genre_tzanetakis:classical"]
Tags.classical_tzanetakis.Type = "float"
Tags.classical_tzanetakis.Album = false
Tags.country.Aliases = ["ab:hi:genre_tzanetakis:country"]
Tags.country.Type = "float"
Tags.country.Album = false
Tags.disco.Aliases = ["ab:hi:genre_tzanetakis:disco"]
Tags.disco.Type = "float"
Tags.disco.Album = false
Tags.hiphop_tzanetakis.Aliases = ["ab:hi:genre_tzanetakis:hiphop"]
Tags.hiphop_tzanetakis.Type = "float"
Tags.hiphop_tzanetakis.Album = false
Tags.jazz_tzanetakis.Aliases = ["ab:hi:genre_tzanetakis:jazz"]
Tags.jazz_tzanetakis.Type = "float"
Tags.jazz_tzanetakis.Album = false
Tags.metal.Aliases = ["ab:hi:genre_tzanetakis:metal"]
Tags.metal.Type = "float"
Tags.metal.Album = false
Tags.pop_tzanetakis.Aliases = ["ab:hi:genre_tzanetakis:pop"]
Tags.pop_tzanetakis.Type = "float"
Tags.pop_tzanetakis.Album = false
Tags.reggae.Aliases = ["ab:hi:genre_tzanetakis:reggae"]
Tags.reggae.Type = "float"
Tags.reggae.Album = false
Tags.rock_tzanetakis.Aliases = ["ab:hi:genre_tzanetakis:rock"]
Tags.rock_tzanetakis.Type = "float"
Tags.rock_tzanetakis.Album = false
Tags.acoustic.Aliases = ["ab:hi:mood_acoustic:acoustic"]
Tags.acoustic.Type = "float"
Tags.acoustic.Album = false
Tags.notacoustic.Aliases = ["ab:hi:mood_acoustic:not acoustic"]
Tags.notacoustic.Type = "float"
Tags.notacoustic.Album = false
Tags.aggressive.Aliases = ["ab:hi:mood_aggressive:aggressive"]
Tags.aggressive.Type = "float"
Tags.aggressive.Album = false
Tags.notaggressive.Aliases = ["ab:hi:mood_aggressive:not aggressive"]
Tags.notaggressive.Type = "float"
Tags.notaggressive.Album = false
Tags.electronic.Aliases = ["ab:hi:mood_electronic:electronic"]
Tags.electronic.Type = "float"
Tags.electronic.Album = false
Tags.notelectronic.Aliases = ["ab:hi:mood_electronic:not electronic"]
Tags.notelectronic.Type = "float"
Tags.notelectronic.Album = false
Tags.happy.Aliases = ["ab:hi:mood_happy:happy"]
Tags.happy.Type = "float"
Tags.happy.Album = false
Tags.nothappy.Aliases = ["ab:hi:mood_happy:not happy"]
Tags.nothappy.Type = "float"
Tags.nothappy.Album = false
Tags.party.Aliases = ["ab:hi:mood_party:party"]
Tags.party.Type = "float"
Tags.party.Album = false
Tags.notparty.Aliases = ["ab:hi:mood_party:not party"]
Tags.notparty.Type = "float"
Tags.notparty.Album = false
Tags.relaxed.Aliases = ["ab:hi:mood_relaxed:relaxed"]
Tags.relaxed.Type = "float"
Tags.relaxed.Album = false
Tags.notrelaxed.Aliases = ["ab:hi:mood_relaxed:not relaxed"]
Tags.notrelaxed.Type = "float"
Tags.notrelaxed.Album = false
Tags.sad.Aliases = ["ab:hi:mood_sad:sad"]
Tags.sad.Type = "float"
Tags.sad.Album = false
Tags.notsad.Aliases = ["ab:hi:mood_sad:not sad"]
Tags.notsad.Type = "float"
Tags.notsad.Album = false
Tags.msd00s.Aliases = ["ab:hi:msd:00s"]
Tags.msd00s.Type = "float"
Tags.msd00s.Album = false
Tags.msd60s.Aliases = ["ab:hi:msd:60s"]
Tags.msd60s.Type = "float"
Tags.msd60s.Album = false
Tags.msd70s.Aliases = ["ab:hi:msd:70s"]
Tags.msd70s.Type = "float"
Tags.msd70s.Album = false
Tags.msd80s.Aliases = ["ab:hi:msd:80s"]
Tags.msd80s.Type = "float"
Tags.msd80s.Album = false
Tags.msd90s.Aliases = ["ab:hi:msd:90s"]
Tags.msd90s.Type = "float"
Tags.msd90s.Album = false
Tags.msdacoustic.Aliases = ["ab:hi:msd:acoustic"]
Tags.msdacoustic.Type = "float"
Tags.msdacoustic.Album = false
Tags.msdalternative.Aliases = ["ab:hi:msd:alternative"]
Tags.msdalternative.Type = "float"
Tags.msdalternative.Album = false
Tags.msdalternativerock.Aliases = ["ab:hi:msd:alternative rock"]
Tags.msdalternativerock.Type = "float"
Tags.msdalternativerock.Album = false
Tags.msdambient.Aliases = ["ab:hi:msd:ambient"]
Tags.msdambient.Type = "float"
Tags.msdambient.Album = false
Tags.msdbeautiful.Aliases = ["ab:hi:msd:beautiful"]
Tags.msdbeautiful.Type = "float"
Tags.msdbeautiful.Album = false
Tags.msdblues.Aliases = ["ab:hi:msd:blues"]
Tags.msdblues.Type = "float"
Tags.msdblues.Album = false
Tags.msdcatchy.Aliases = ["ab:hi:msd:catchy"]
Tags.msdcatchy.Type = "float"
Tags.msdcatchy.Album = false
Tags.msdchill.Aliases = ["ab:hi:msd:chill"]
Tags.msdchill.Type = "float"
Tags.msdchill.Album = false
Tags.msdchillout.Aliases = ["ab:hi:msd:chillout"]
Tags.msdchillout.Type = "float"
Tags.msdchillout.Album = false
Tags.msdclassicrock.Aliases = ["ab:hi:msd:classic rock"]
Tags.msdclassicrock.Type = "float"
Tags.msdclassicrock.Album = false
Tags.msdcountry.Aliases = ["ab:hi:msd:country"]
Tags.msdcountry.Type = "float"
Tags.msdcountry.Album = false
Tags.msddance.Aliases = ["ab:hi:msd:dance"]
Tags.msddance.Type = "float"
Tags.msddance.Album = false
Tags.msdeasylistening.Aliases = ["ab:hi:msd:easy listening"]
Tags.msdeasylistening.Type = "float"
Tags.msdeasylistening.Album = false
Tags.msdelectro.Aliases = ["ab:hi:msd:electro"]
Tags.msdelectro.Type = "float"
Tags.msdelectro.Album = false
Tags.msdelectronic.Aliases = ["ab:hi:msd:electronic"]
Tags.msdelectronic.Type = "float"
Tags.msdelectronic.Album = false
Tags.msdelectronica.Aliases = ["ab:hi:msd:electronica"]
Tags.msdelectronica.Type = "float"
Tags.msdelectronica.Album = false
Tags.msdexperimental.Aliases = ["ab:hi:msd:experimental"]
Tags.msdexperimental.Type = "float"
Tags.msdexperimental.Album = false
Tags.msdfemalevocalist.Aliases = ["ab:hi:msd:female vocalist"]
Tags.msdfemalevocalist.Type = "float"
Tags.msdfemalevocalist.Album = false
Tags.msdfemalevocalists.Aliases = ["ab:hi:msd:female vocalists"]
Tags.msdfemalevocalists.Type = "float"
Tags.msdfemalevocalists.Album = false
Tags.msdfolk.Aliases = ["ab:hi:msd:folk"]
Tags.msdfolk.Type = "float"
Tags.msdfolk.Album = false
Tags.msdfunk.Aliases = ["ab:hi:msd:funk"]
Tags.msdfunk.Type = "float"
Tags.msdfunk.Album = false
Tags.msdguitar.Aliases = ["ab:hi:msd:guitar"]
Tags.msdguitar.Type = "float"
Tags.msdguitar.Album = false
Tags.msdhappy.Aliases = ["ab:hi:msd:happy"]
Tags.msdhappy.Type = "float"
Tags.msdhappy.Album = false
Tags.msdhardrock.Aliases = ["ab:hi:msd:hard rock"]
Tags.msdhardrock.Type = "float"
Tags.msdhardrock.Album = false
Tags.msdheavymetal.Aliases = ["ab:hi:msd:heavy metal"]
Tags.msdheavymetal.Type = "float"
Tags.msdheavymetal.Album = false
Tags.msdhiphop.Aliases = ["ab:hi:msd:hip-hop"]
Tags.msdhiphop.Type = "float"
Tags.msdhiphop.Album = false
Tags.msdhouse.Aliases = ["ab:hi:msd:house"]
Tags.msdhouse.Type = "float"
Tags.msdhouse.Album = false
Tags.msdindie.Aliases = ["ab:hi:msd:indie"]
Tags.msdindie.Type = "float"
Tags.msdindie.Album = false
Tags.msdindiepop.Aliases = ["ab:hi:msd:indie pop"]
Tags.msdindiepop.Type = "float"
Tags.msdindiepop.Album = false
Tags.msdindierock.Aliases = ["ab:hi:msd:indie rock"]
Tags.msdindierock.Type = "float"
Tags.msdindierock.Album = false
Tags.msdinstrumental.Aliases = ["ab:hi:msd:instrumental"]
Tags.msdinstrumental.Type = "float"
Tags.msdinstrumental.Album = false
Tags.msdjazz.Aliases = ["ab:hi:msd:jazz"]
Tags.msdjazz.Type = "float"
Tags.msdjazz.Album = false
Tags.msdmalevocalists.Aliases = ["ab:hi:msd:male vocalists"]
Tags.msdmalevocalists.Type = "float"
Tags.msdmalevocalists.Album = false
Tags.msdmellow.Aliases = ["ab:hi:msd:mellow"]
Tags.msdmellow.Type = "float"
Tags.msdmellow.Album = false
Tags.msdmetal.Aliases = ["ab:hi:msd:metal"]
Tags.msdmetal.Type = "float"
Tags.msdmetal.Album = false
Tags.msdoldies.Aliases = ["ab:hi:msd:oldies"]
Tags.msdoldies.Type = "float"
Tags.msdoldies.Album = false
Tags.msdparty.Aliases = ["ab:hi:msd:party"]
Tags.msdparty.Type = "float"
Tags.msdparty.Album = false
Tags.msdpop.Aliases = ["ab:hi:msd:pop"]
Tags.msdpop.Type = "float"
Tags.msdpop.Album = false
Tags.msdprogressiverock.Aliases = ["ab:hi:msd:progressive rock"]
Tags.msdprogressiverock.Type = "float"
Tags.msdprogressiverock.Album = false
Tags.msdpunk.Aliases = ["ab:hi:msd:punk"]
Tags.msdpunk.Type = "float"
Tags.msdpunk.Album = false
Tags.msdrnb.Aliases = ["ab:hi:msd:rnb"]
Tags.msdrnb.Type = "float"
Tags.msdrnb.Album = false
Tags.msdrock.Aliases = ["ab:hi:msd:rock"]
Tags.msdrock.Type = "float"
Tags.msdrock.Album = false
Tags.msdsad.Aliases = ["ab:hi:msd:sad"]
Tags.msdsad.Type = "float"
Tags.msdsad.Album = false
Tags.msdsexy.Aliases = ["ab:hi:msd:sexy"]
Tags.msdsexy.Type = "float"
Tags.msdsexy.Album = false
Tags.msdsoul.Aliases = ["ab:hi:msd:soul"]
Tags.msdsoul.Type = "float"
Tags.msdsoul.Album = false
Tags.atonal.Aliases = ["ab:hi:tonal_atonal:atonal"]
Tags.atonal.Type = "float"
Tags.atonal.Album = false
Tags.tonal.Aliases = ["ab:hi:tonal_atonal:tonal"]
Tags.tonal.Type = "float"
Tags.tonal.Album = false
Tags.instrumental.Aliases = ["ab:hi:voice_instrumental:instrumental"]
Tags.instrumental.Type = "float"
Tags.instrumental.Album = false
Tags.voice.Aliases = ["ab:hi:voice_instrumental:voice"]
Tags.voice.Type = "float"
Tags.voice.Album = false
Tags.chordschangesrate.Aliases = ["ab:lo:tonal:chords_changes_rate"]
Tags.chordschangesrate.Type = "float"
Tags.chordschangesrate.Album = false
Tags.chordskey.Aliases = ["ab:lo:tonal:chords_key"]
Tags.chordskey.Type = "string"
Tags.chordskey.Album = false
Tags.chordscale.Aliases = ["ab:lo:tonal:chords_scale"]
Tags.chordscale.Type = "string"
Tags.chordscale.Album = false
Tags.keykey.Aliases = ["ab:lo:tonal:key_key"]
Tags.keykey.Type = "string"
Tags.keykey.Album = false
Tags.keyscale.Aliases = ["ab:lo:tonal:key_scale"]
Tags.keyscale.Type = "string"
Tags.keyscale.Album = false

115
misc/tags.txt Normal file
View File

@@ -0,0 +1,115 @@
Below is a list of all the tags this plugin generates:
bpm
key
mood
tags
ab:hi:danceability:danceable
ab:hi:danceability:not danceable
ab:hi:gender:female
ab:hi:gender:male
ab:hi:genre_electronic:ambient
ab:hi:genre_electronic:drum and bass
ab:hi:genre_electronic:house
ab:hi:genre_electronic:techno
ab:hi:genre_electronic:trance
ab:hi:genre_rosamerica:classical
ab:hi:genre_rosamerica:dance
ab:hi:genre_rosamerica:hiphop
ab:hi:genre_rosamerica:jazz
ab:hi:genre_rosamerica:pop
ab:hi:genre_rosamerica:rhythm and blues
ab:hi:genre_rosamerica:rock
ab:hi:genre_rosamerica:speech
ab:hi:genre_tzanetakis:blues
ab:hi:genre_tzanetakis:classical
ab:hi:genre_tzanetakis:country
ab:hi:genre_tzanetakis:disco
ab:hi:genre_tzanetakis:hiphop
ab:hi:genre_tzanetakis:jazz
ab:hi:genre_tzanetakis:metal
ab:hi:genre_tzanetakis:pop
ab:hi:genre_tzanetakis:reggae
ab:hi:genre_tzanetakis:rock
ab:hi:mood_acoustic:acoustic
ab:hi:mood_acoustic:not acoustic
ab:hi:mood_aggressive:aggressive
ab:hi:mood_aggressive:not aggressive
ab:hi:mood_electronic:electronic
ab:hi:mood_electronic:not electronic
ab:hi:mood_happy:happy
ab:hi:mood_happy:not happy
ab:hi:mood_party:party
ab:hi:mood_party:not party
ab:hi:mood_relaxed:relaxed
ab:hi:mood_relaxed:not relaxed
ab:hi:mood_sad:sad
ab:hi:mood_sad:not sad
ab:hi:msd:00s
ab:hi:msd:60s
ab:hi:msd:70s
ab:hi:msd:80s
ab:hi:msd:90s
ab:hi:msd:acoustic
ab:hi:msd:alternative
ab:hi:msd:alternative rock
ab:hi:msd:ambient
ab:hi:msd:beautiful
ab:hi:msd:blues
ab:hi:msd:catchy
ab:hi:msd:chill
ab:hi:msd:chillout
ab:hi:msd:classic rock
ab:hi:msd:country
ab:hi:msd:dance
ab:hi:msd:easy listening
ab:hi:msd:electro
ab:hi:msd:electronic
ab:hi:msd:electronica
ab:hi:msd:experimental
ab:hi:msd:female vocalist
ab:hi:msd:female vocalists
ab:hi:msd:folk
ab:hi:msd:funk
ab:hi:msd:guitar
ab:hi:msd:happy
ab:hi:msd:hard rock
ab:hi:msd:heavy metal
ab:hi:msd:hip-hop
ab:hi:msd:house
ab:hi:msd:indie
ab:hi:msd:indie pop
ab:hi:msd:indie rock
ab:hi:msd:instrumental
ab:hi:msd:jazz
ab:hi:msd:male vocalists
ab:hi:msd:mellow
ab:hi:msd:metal
ab:hi:msd:oldies
ab:hi:msd:party
ab:hi:msd:pop
ab:hi:msd:progressive rock
ab:hi:msd:punk
ab:hi:msd:rnb
ab:hi:msd:rock
ab:hi:msd:sad
ab:hi:msd:sexy
ab:hi:msd:soul
ab:hi:tonal_atonal:atonal
ab:hi:tonal_atonal:tonal
ab:hi:voice_instrumental:instrumental
ab:hi:voice_instrumental:voice
ab:lo:tonal:chords_changes_rate
ab:lo:tonal:chords_key
ab:lo:tonal:chords_scale
ab:lo:tonal:key_key
ab:lo:tonal:key_scale
replaygain_track_gain
replaygain_track_peak
replaygain_track_range
replaygain_album_gain
replaygain_album_peak
replaygain_album_range
replaygain_reference_loudness
r128_track_gain
r128_album_gain