Compare commits
5 Commits
c62f7f9e31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e6062ca13f
|
|||
|
09ad6d997b
|
|||
|
1d6d3405c3
|
|||
|
f43eeaafe0
|
|||
|
a8794fa239
|
575
__init__.py
575
__init__.py
@@ -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()
|
||||
|
||||
|
||||
BIN
bin/Qt5Core.dll
BIN
bin/Qt5Core.dll
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12
constants.py
12
constants.py
@@ -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
1
misc/llms/README.md
Normal 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
351
misc/llms/navidrome.md
Normal 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 Navidrome’s 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
404
misc/navidrome.toml
Normal 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
115
misc/tags.txt
Normal 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
|
||||
Reference in New Issue
Block a user