mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-24 22:02:09 +00:00
* Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
"""Persist user-edited song metadata back into the underlying song file.
|
|
|
|
The library scanner (`lib/scan_worker.py`) re-derives title/artist/album/year
|
|
from the file on every full rescan — from the sloppak ``manifest.yaml``
|
|
top-level keys. A DB-only edit therefore reverts the moment a full rescan
|
|
re-reads the file. Writing the edit into the file makes the file the single
|
|
source of truth, so the change survives both incremental and full rescans.
|
|
|
|
``fields`` is a partial dict of any of ``title``/``artist``/``album``/``year``;
|
|
only the keys present are overwritten, so an edit of just the title can't blank
|
|
out the artist.
|
|
|
|
Only feedBack's own ``.sloppak`` format (zip- or directory-form) is writable.
|
|
Unknown / unsupported shapes return False and the caller keeps the DB-only
|
|
update.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
def _coerce_year(value):
|
|
"""Convert *value* to an int year, or 0 for empty/invalid (clear intent).
|
|
|
|
The scanner reads ``SongYear`` as ``str(manifest.get("year", "") or "")``
|
|
for sloppaks — so 0 round-trips back to an empty string, which is the
|
|
correct DB representation of "no year". Callers must gate on
|
|
``"year" in fields`` before calling this; they must NOT gate on the return
|
|
value being non-None/non-zero (that would silently drop a year-clear edit,
|
|
which is the bug this function fixes).
|
|
"""
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return 0 # empty string / non-numeric → clear the year
|
|
|
|
|
|
def _apply_to_sloppak_manifest(manifest: dict, fields: dict) -> bool:
|
|
"""Update a parsed sloppak manifest in place. Returns True if changed."""
|
|
dirty = False
|
|
for key in ("title", "artist", "album"):
|
|
if fields.get(key) is not None:
|
|
manifest[key] = str(fields[key])
|
|
dirty = True
|
|
if "year" in fields:
|
|
manifest["year"] = _coerce_year(fields["year"])
|
|
dirty = True
|
|
# Opportunistically declare the format version (spec §4) when we're already
|
|
# rewriting because a metadata field was supplied. Gated on `dirty` (i.e. a
|
|
# field was given) so this never forces a *standalone* rewrite with no fields
|
|
# passed, and `not in` so an existing (possibly higher) version is preserved,
|
|
# never downgraded. NB `dirty` here means "a field was supplied" — a
|
|
# supplied-but-identical value already triggers a rewrite (pre-existing).
|
|
if dirty and "feedpak_version" not in manifest:
|
|
from sloppak import FEEDPAK_VERSION
|
|
manifest["feedpak_version"] = FEEDPAK_VERSION
|
|
return dirty
|
|
|
|
|
|
def _rewrite_zip_manifest(zip_path: Path, dumped: str) -> bool:
|
|
"""Rewrite manifest.yaml inside a zip-form sloppak, preserving every other
|
|
entry (and its original compression). Backup + temp + atomic replace."""
|
|
zip_path = Path(zip_path)
|
|
backup = zip_path.with_name(zip_path.name + ".bak")
|
|
if not backup.exists():
|
|
shutil.copy2(zip_path, backup)
|
|
out_tmp = zip_path.with_name(zip_path.name + ".tmp")
|
|
with zipfile.ZipFile(str(zip_path), "r") as zin:
|
|
names = zin.namelist()
|
|
manifest_name = "manifest.yaml"
|
|
for cand in ("manifest.yaml", "manifest.yml"):
|
|
if cand in names:
|
|
manifest_name = cand
|
|
break
|
|
with zipfile.ZipFile(str(out_tmp), "w", zipfile.ZIP_DEFLATED) as zout:
|
|
for item in zin.infolist():
|
|
if item.filename in ("manifest.yaml", "manifest.yml"):
|
|
continue
|
|
# Passing the original ZipInfo preserves each entry's
|
|
# compress_type — important for already-compressed ogg stems.
|
|
zout.writestr(item, zin.read(item.filename))
|
|
zout.writestr(manifest_name, dumped)
|
|
out_tmp.replace(zip_path)
|
|
return True
|
|
|
|
|
|
def write_sloppak_metadata(path: Path, fields: dict) -> bool:
|
|
"""Write metadata into a sloppak (directory or zip form). Returns True if
|
|
anything was written."""
|
|
import sloppak as sloppak_mod
|
|
|
|
path = Path(path)
|
|
manifest = sloppak_mod.load_manifest(path)
|
|
if not _apply_to_sloppak_manifest(manifest, fields):
|
|
return False
|
|
dumped = yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True)
|
|
if path.is_dir():
|
|
mf = path / "manifest.yaml"
|
|
if not mf.exists() and (path / "manifest.yml").exists():
|
|
mf = path / "manifest.yml"
|
|
mf.write_text(dumped, encoding="utf-8")
|
|
return True
|
|
return _rewrite_zip_manifest(path, dumped)
|
|
|
|
|
|
def write_song_metadata(path: Path, fields: dict) -> bool:
|
|
"""Persist edited title/artist/album/year into the song's file.
|
|
|
|
Dispatches by shape: ``.sloppak`` files and sloppak directories
|
|
(manifest.yaml present). Loose-folder and unknown shapes return False
|
|
(caller keeps the DB-only update). Returns True if the file was modified.
|
|
"""
|
|
path = Path(path)
|
|
suffix = path.suffix.lower()
|
|
if path.is_dir():
|
|
if (path / "manifest.yaml").exists() or (path / "manifest.yml").exists():
|
|
return write_sloppak_metadata(path, fields)
|
|
return False
|
|
if suffix == ".sloppak":
|
|
return write_sloppak_metadata(path, fields)
|
|
return False
|