Files
just-music-premium/docs/BRIDGE-SIGNALS.md
Barış Keser 642072111f docs: v1.3.0 self-docs — crossfade/gapless + Discord slots & architecture
- BRIDGE-SIGNALS: yeni "Transitions & Discord" slot bolumu (setCrossfade/setGapless/
  setDiscordRpc/setDiscordClientId), getState semasina crossfade/gapless/discord*/
  discordAvail; YANLIS "setLanguage yok" notu duzeltildi (v1.1.0'da eklendi)
- ARCHITECTURE: 6b bolumu — siradaki parcayi onden coz, callback'te es-guclu harman/
  gapless terfi, reload'suz UI guncelleme, Discord RPC tasarimi
2026-09-20 17:20:40 +03:00

17 KiB
Raw Permalink Blame History

Bridge Signal / Slot Catalog (QWebChannel)

Reference for the QWebChannel contract between the Python Bridge (justmusic/bridge.py) and the web UI (justmusic/web/app.js).

  • Signals travel Python → JS. Each is a pyqtSignal class attribute on Bridge, wired to a JS handler in wireSignals() (app.js:710).
  • Slots are called JS → Python as bridge.<method>(...). Each is a @pyqtSlot(...) method on Bridge.
  • JSON-carrying signals/slots pass a string; the schemas below come from the Python builder functions.

Argument types in this doc are the declared pyqtSignal/pyqtSlot signatures. QWebChannel marshals Python dict/list only when serialized to a JSON string first — every "JSON" payload here is a str on the wire.


1. Signals (Python → JS)

Signal Args (declared) Payload / meaning JS handler in wireSignals()
stateChanged () No payload. "State is dirty, re-pull." Handler calls bridge.getState(...). refreshState
trackChanged (str) JSON track object (schema below), or {"none": true} when nothing is loaded. Fires on every now-playing change and same-song metadata updates. j => track = JSON.parse(j); onTrackChanged()
playingChanged (bool) true/false play state. Only emitted when it flips (_last_playing guard). p => active.playing=p; updatePlayBtn(); if(clip) clipMirrorPlay(p)
positionChanged (float, float) (pos_sec, dur_sec). The single clock — drives the seek bar and, in clip mode, the muted video + LRC sync. (pos,dur) => updateSeek(pos,dur); if(clip){syncClipVideo(pos); clipSyncLyrics(pos)}
spectrumSignal (str) JSON array of ~56 floats (rounded to 3 dp), the FFT bars. Emitted every poll (60 ms). j => spectrum = JSON.parse(j)
coverReadySignal (str, str) (song_id, cover_url). URL form: app://app/cover/<md5(song_id)>.jpg. (id,url) => updateCovers(id,url)
toastSignal (str) Plain message string for the toast. showToast
downloadProgressSignal (float, str, int) (pct, status, queue_remaining). status carries the batch counter "X/Y" (see notes). (0, "", 0) means the queue drained. (pct,status,q) => updateDlPill(pct,status,q)
downloadDoneSignal (str) JSON of the finished song = _song_view() shape. () => {} (no-op; UI refreshes via stateChanged)
searchResultsSignal (str) JSON array of search rows (schema below), or {"error": "<msg>"}. Used by both searchYouTube and importPlaylist. onSearchResults
scanDoneSignal (int, int) (songs_added, new_playlists) from a music-folder scan. (none — not connected; see note)
durationSignal (str, int) (song_id, duration_ms) — late-probed track length. onDuration
queueChanged (str) JSON array of up-next entries = _song_view() + playlist field; "[]" when cleared. onQueue
videoReadySignal (str) JSON clip stream object (schema below). onVideoReady
analysisSignal (str) JSON song-DNA object (schema below). onAnalysis
loopSignal (float, float) (A_sec, B_sec) A-B loop points. -1 in a slot = unset (e.g. (-1,-1) clears, (A,-1) = only A set). onLoop
updateAvailableSignal (str, str) (version, notes) — a newer release exists. (v,notes) => updateInfo={version:v,notes:notes}
updateProgressSignal (float) Download percent of the staged .msi. p => onUpdateProgress(p)
updateReadySignal (str) version — update downloaded, installs on exit (or via installUpdateNow). v => onUpdateReady(v)
separationProgressSignal (float, str) (pct, status) for htdemucs vocal separation. (100.0, "Hazır") = done, (100.0, "") = failed. (pct,status) => updateSepPill(pct,status)
karaokeModeSignal (str) Current mode: off / quick / instrumental / vocals. m => onKaraokeMode(m)

Note on scanDoneSignal: it is declared and emitted (_on_scan_done), but is not connected inside wireSignals(); scan results reach the UI mainly through the toastSignal + stateChanged that fire alongside it.

JSON payload schemas

trackChanged — _track_json() (built from _song_view() + extra fields):

{
  "id": "...", "title": "...", "artist": "...",
  "favorite": false,
  "duration_ms": 0, "play_count": 0,
  "source": "local",            // or "youtube"
  "cover": "app://app/cover/<md5>.jpg",
  "hasCover": true,             // thumbnail file exists on disk
  "mood": "", "bpm": 0,
  "playlist": "<active playlist>",
  "lyrics": "",                 // may be LRC ([mm:ss] timestamped)
  "shuffle": false,
  "repeat": false,              // == (repeat_mode == 2)
  "repeat_mode": 1              // 0=off, 1=list, 2=one
}
// when nothing is loaded:
{ "none": true }

stateChanged → getState() (JS pulls it via the getState slot; full app state):

{
  "playlists": { "<name>": [ /* _song_view objects */ ] },
  "protected": ["<protected playlist names>"],
  "version": "<APP_VERSION>",
  "current": "<current playlist>",
  "settings": { /* library.settings: theme, accent, lang, volume, speed,
                   eq_gains, eq_enabled, eq_preset, effects, normalize,
                   soundscape, crossfade, gapless, discord_rpc, discord_client_id,
                   recent, playlist_covers, playlist_desc, ... */ },
  "presets": { "<preset name>": [10 EQ gains] },
  "presetNames": ["<ordered preset names>"],
  "bandLabels": ["<10 EQ band labels>"],
  "effectDefaults": { "<effect>": <default> },
  "effectDefsOrder": ["<effect keys in order>"],
  "active": { "playlist": "...", "song_id": "...", "playing": false,
              "shuffle": false, "repeat": false, "repeat_mode": 1 },
  "stats": { "total_seconds": 0, ... },
  "recent": [ /* _song_view objects, each with an added "playlist" field */ ],
  "favoritesName": "<favorites playlist name>",
  "demucs": false,             // studio-karaoke (htdemucs) engine available?
  "karaokeMode": "off",
  "discordAvail": false        // pypresence importable? (Discord RPC available) — v1.3.0
}

_song_view() (the per-song shape reused in playlists, recent, queueChanged, downloadDoneSignal):

{
  "id": "...", "title": "...", "artist": "...",
  "favorite": false, "duration_ms": 0, "play_count": 0,
  "source": "local", "cover": "app://app/cover/<md5>.jpg",
  "hasCover": true, "mood": "", "bpm": 0
}

(queueChanged and recent entries additionally carry a "playlist" field.)

searchResultsSignal — search rows (SearchThread.run, also importPlaylist):

[
  { "id": "<videoId>", "title": "...", "uploader": "...",
    "duration_ms": 0,
    "thumbnail": "https://i.ytimg.com/vi/<id>/hqdefault.jpg",
    "url": "https://www.youtube.com/watch?v=<id>" }
]
// on failure:
{ "error": "<message>" }

videoReadySignal — clip stream (VideoStreamThread + _on_video_ready):

{
  "id": "<song_id>",
  "video": "<webm VP9 video-only stream URL>",
  "audio": "<opus audio-only stream URL>",  // may be empty
  "title": "...", "duration": 0,
  "start": 12.34,     // seconds; handoff position injected by _on_video_ready
  "muted": true       // ALWAYS true — audio comes from the DSP engine, not the video
}

analysisSignal — song DNA (AnalyzeThread):

{
  "id": "<song_id>",
  "waveform": [ /* 480 peak floats */ ],
  "mood": "<label>", "color": "<hex>",
  "bpm": 0, "gain": 1.0,        // gain feeds loudness normalize
  "spectrogram": [ /* rows */ ]
}

2. Slots (JS → Python)

Called as bridge.<name>(args). Slots that return a value (result=str) are invoked with a callback in JS: bridge.getState(json => {...}).

Playback / transport

Slot Args Purpose
play (song_id: str, playlist: str) Play a specific song from a playlist.
toggle () Play/pause; if nothing loaded, starts the current playlist.
playPause () Alias for toggle.
next () Next track (honors shuffle).
prev () Previous track (or restart if >3 s in).
seek (seconds: float) Seek the engine to an absolute position.
setShuffle (on: bool) Toggle shuffle.
setRepeat (on: bool) Toggle repeat-one (legacy boolean).
setRepeatMode (mode: int) Cycle repeat: 0=off, 1=list, 2=one (mode % 3).
setVolume (v: float) Base volume 0–150 (persisted when >0).
setSpeed (r: float) Playback speed (0.5–2.0).
setNormalize (on: bool) Loudness normalize on/off.
setLoopA () Set A-B loop start = current position.
setLoopB () Set A-B loop end = current position.
clearLoop () Clear the A-B loop.
playPlaylistShuffled (playlist: str, _unused: str="") Turn on shuffle and start the playlist at a random track.

Queue (up-next)

Slot Args Purpose
addToQueue (sid: str, playlist: str) Append a track to the user queue.
playNext (sid: str, playlist: str) Insert a track at the front of the queue.
addAllToQueue (playlist: str) Queue an entire playlist.
startRadio (sid: str, playlist: str) Queue ~20 mood-similar tracks after the seed.
getQueue () → str JSON of the queue (_song_view + playlist). Callback.
clearQueue () Empty the queue.
removeFromQueue (index: int) Drop the queue item at index.
playQueueIndex (index: int) Pop and play the queue item at index.

Playlists

Slot Args Purpose
selectPlaylist (name: str) Set the current playlist (no playback change).
addPlaylist (name: str) Create a playlist.
deletePlaylist (name: str) Delete a playlist.
renamePlaylist (old: str, new: str) Rename (protected lists blocked).
reorderSong (playlist: str, frm: int, to: int) Drag-reorder within a playlist.
setPlaylistCover (playlist: str, url: str) Set a custom cover URL.
setPlaylistDescription (playlist: str, text: str) Set the playlist description.
removeDuplicates (playlist: str) Drop duplicate file paths.
exportM3U (playlist: str) Save playlist as .m3u (native file dialog).

Song actions

Slot Args Purpose
toggleFavorite (song_id: str, playlist: str) Toggle favorite flag.
removeSong (song_id: str, playlist: str) Remove a song from a playlist.
copyToPlaylist (song_id: str, playlist: str, target: str) Copy a song into another playlist.
editMetadata (song_id: str, playlist: str, title: str, artist: str) Edit title/artist.
openFileLocation (sid: str, playlist: str) Reveal the file in Explorer.
exportTrim (sid: str, playlist: str, a: float, b: float) Export the A–B segment to MP3 (ffmpeg).

Search / download

Slot Args Purpose
searchYouTube (query: str) YouTube search (20 rows) → searchResultsSignal.
importPlaylist (url: str) Resolve a YouTube playlist URL into preview-and-select rows → searchResultsSignal.
downloadUrls (urls_json: str) Enqueue one or many URLs (JSON array, or a single string) for download; grows the "X/Y" batch counter.

Effects / EQ

Slot Args Purpose
setEq (gains_json: str) Set 10-band EQ gains (JSON array).
setEqPreset (name: str) Persist selected preset name.
setEqEnabled (on: bool) Enable/disable EQ.
autoEq () → str Auto-compute EQ from current audio; returns JSON gains. Callback.
setEffect (name: str, val: float) Set one effect (preamp/bass/echo/reverb/spatial/…).
setSoundscape (kind: str, level: float) Ambient soundscape (rain/white/brown/off).

Karaoke / vocal separation

Slot Args Purpose
setKaraoke (mode: str) off / quick (instant mid-side) / instrumental / vocals (htdemucs stems). Falls back to quick when demucs is unavailable; drives karaokeModeSignal + separationProgressSignal.

Transitions & Discord (v1.3.0)

Slot Args Purpose
setCrossfade (sec: float) Crossfade length 0–12s (0 = off). Engine pre-decodes the next sequential track and equal-power blends over the last sec seconds. Persists settings.crossfade.
setGapless (on: bool) Seamless boundary join (no gap, no overlap) when a next track is preloaded. Persists settings.gapless.
setDiscordRpc (on: bool) Enable/disable Discord Rich Presence. Needs a Client ID first (else a toast). Persists settings.discord_rpc.
setDiscordClientId (cid: str) Store the Discord Application ID (settings.discord_client_id); reconnects if RPC is on.

Engine mechanics: the transition only fires for predictable sequential playback — not when shuffle is on, user_queue is non-empty, or repeat_mode == 2 (repeat-one). The poll (_maybe_preload_next) calls engine.preload_next(path) when dur - pos ≤ crossfade + 8s; the engine promotes next→current inside the audio callback (_promote_next) and flags it via consume_advanced(), which the poll turns into a gapless UI update (_on_crossfade_advanced) with no reload. Default off = byte-for-byte the old behavior. See docs/ARCHITECTURE.md.

Clip (music-video) mode

Slot Args Purpose
playVideo (sid: str, playlist: str) Resolve a muted webm video stream synced to the DSP engine → videoReadySignal.
resumeMusic (pos: float=-1.0) Called when the clip closes; ensures the engine is still playing (engine never stopped). JS calls resumeMusic(-1).

Lyrics

Slot Args Purpose
fetchLyrics (sid: str, playlist: str) Fetch timestamped (LRC) lyrics from Lyrica; result lands via trackChanged.
saveLyrics (song_id: str, playlist: str, text: str) Save manually edited lyrics.

Import / scan / covers

Slot Args Purpose
importFiles () Open a file dialog and import audio into the current playlist.
scanMusic () Scan the music folder in a background thread → scanDoneSignal.
syncNamesCovers (playlist: str) Re-derive title/artist from filenames and re-fetch covers.
requestCovers (playlist: str) Fetch missing covers + probe durations for a playlist.
requestSidebarCovers () Fetch covers for the first few songs of every playlist.

Updates (auto-update)

Slot Args Purpose
installUpdateNow () Install the staged .msi and relaunch immediately.

State / theme / misc

Slot Args Purpose
getState () → str Full app state JSON (schema above). Callback.
setTheme (name: str) Set the theme (clears custom accent).
setAccent (hex_color: str) Set a custom accent color.
setLanguage (code: str) Persist UI language (tr/en). Since v1.1.0; display-only translate-by-source via web/i18n.js T() — internal data stays Turkish.
backup () Save a JSON library backup (native file dialog).

i18n note (v1.1.0+): localization is translate-by-source — the UI calls T(turkishString) from web/i18n.js; only display text is translated, internal logic/data (mood labels, protected-playlist keys, status matching) stays Turkish. Add a language by extending LANGS in i18n.js, no Python change needed.


3. Non-obvious contract facts

  • downloadProgressSignal(pct, status, queue_remaining) — the status string now carries "X/Y". _dl_counter() appends done/total (e.g. "İndiriliyor… 2/5") whenever a batch of more than one URL is queued via downloadUrls. queue_remaining is len(download_queue) after the current item was popped. When the queue drains, (0, "", 0) is emitted to clear the pill.

  • positionChanged(pos, dur) is the single clock. The engine is the sole audio source; in clip mode the muted video is driven off this same signal (syncClipVideo(pos)), and LRC lyric highlighting rides it too (clipSyncLyrics / syncLyrics). There is no second timeline for the video.

  • trackChanged fires for same-song metadata updates too. It is emitted not only on song change but also for late-arriving lyrics (_on_lyrics), metadata edits (editMetadata), and name/cover sync (syncNamesCovers) — always for the currently active song. onTrackChanged() must tolerate this: it compares track.id === active.song_id and, if the song is unchanged, refreshes clip lyrics instead of tearing down the clip.

  • clip/video audio is always muted ("muted": true forced in _on_video_ready); all DSP effects therefore apply to the clip because the sound still comes from the engine, avoiding double playback.

  • loopSignal / progress sentinels: loopSignal uses -1 for "unset" ((-1,-1) = cleared). separationProgressSignal uses (100.0, "Hazır") for success and (100.0, "") for failure.

  • Callback-style slots: getState, getQueue, and autoEq are result=str and are called with a JS callback (bridge.getState(json => ...)), not synchronously.