Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

GUI: make the Shuffle button on Liked Songs and playlist pages work

Aperta Adatta ai principianti
#841 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
78/100
Tipo di issue
Funzionalità
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
react, typescript
Ambito
frontend

Direzione di ricerca

Inizia da gui/src/ListPage.tsx, dal pulsante Shuffle disabilitato (righe 226-233) e dalla lista shownTracks (righe 126-129). Aggiungi in gui/src/sortModel.ts, accanto a shownPlayRequest, un helper puro shufflePlayRequest con una funzione casuale iniettabile, e collegalo all'onClick del pulsante. È completato quando npm test -- sortModel e npm test -- listPage passano con nuovi casi e i controlli completi in gui/ passano.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

enhancement good first issue javascript

What

Open Liked Songs or any playlist in the GUI. Next to the big green Play button there's a Shuffle button. It's always greyed out, even when the list is full of songs. It was added as a placeholder and never wired up.

To see it:

  1. Start the GUI (see "Done when" for how). From Your Library, open Liked Songs or a playlist. With Local Files, open a folder.
  2. Hover over the Shuffle icon next to Play, or click it. Nothing happens, because it's disabled.

What should happen: clicking Shuffle plays the songs you see on the page in a random order. If you typed something in the Filter box, it shuffles only the matching rows, the same rows Play would use.

All of this happens in the page. It picks a random order with a Fisher-Yates shuffle and sends the same "play this list of URIs" message that Play already sends after a sort or filter. No Rust change is needed.

Shuffle uses the rows that are loaded when you click. You don't need to fetch anything first. Once the first page of Liked Songs or a Spotify playlist arrives, spotatui fetches the remaining pages in the background by itself, so the list fills up within moments. The header already tells you when it isn't complete yet ("120+ songs", or "500 songs, 120 loaded").

Where to look

  • gui/src/ListPage.tsx, lines 226-233. This is the button. Line 230 hard-codes disabled, and there's no onClick:
    <button
      type="button"
      className="list-shuffle"
      aria-label="Shuffle"
      disabled
    >
      <Glyph name="shuffle" size={28} />
    </button>
    
  • Same file, the Play button just above it (lines 220-225). Its label is `Play ${title}` (title is set at line 182), and it's disabled when first < 0. first (line 205) is shownTracks.findIndex((track) => track.uri), the first shown row that has a URI.
  • Same file, lines 126-129: shownTracks holds the rows as the table draws them, after the sort and the filter. That's the list to shuffle.
  • Same file, lines 132-138: play(index) builds its request with shownPlayRequest and calls send(action).
  • Same file, lines 172-177: this is where the page asks for the next page itself, with send({ LoadMore: ... }), when you step past the last row. You won't need it for Shuffle.
  • gui/src/sortModel.ts, shownPlayRequest (lines 148-157). For a Spotify playlist in its loaded order, it sends PlayTrackInContext. Once the rows are sorted or filtered, it sends playRequest(shown, index) instead.
  • gui/src/libraryModel.ts, playRequest (lines 109-117). Shuffle should send this same shape:
    const uris = tracks.flatMap((track) => (track.uri ? [track.uri] : []));
    const offset = tracks.slice(0, cursor).filter((track) => track.uri).length;
    return { PlayUris: { uris, offset } };
    
    A row counts as playable when it has a uri. The page never reads is_playable, so don't add that check here either.
  • gui/src/App.tsx, lines 13-16: send wraps every Action as { type: "action", action }. That's ClientMessage::Action in src/gui/protocol.rs (lines 590-592). So PlayUris is an existing Action, not a separate client message, and the page can already send it.
  • src/core/action/mod.rs, lines 95-98: Action::PlayUris { uris, offset }. src/core/action/apply.rs line 68 hands it to App::start_playback_uris (src/core/app/transport.rs, lines 465-480), which routes it to whichever source the URIs belong to.
  • src/core/action/mod.rs, line 83: Action::SetShuffle(bool). Its arm in apply.rs (lines 49-56) reads the shuffle state of whatever is playing right now and toggles that player. The traps below explain why that matters.
  • src/infra/network/playback.rs, line 32 (MAX_API_PLAYBACK_URIS: usize = 100) and trim_api_playback_uris (lines 90-115). When Spotify starts from a URI list, it keeps at most 100 URIs around the offset. With offset 0, that's the first 100 of your shuffled order.
  • gui/src/ListPage.css, lines 24-35: .list-shuffle already has a hover style for the enabled state, so you don't need any new CSS.
  • Tests: gui/src/sortModel.test.ts has a track(name, artists, album, duration_ms, uri) helper where uri can be null (lines 16-37). Its shownPlayRequest block (lines 204-229) is a good model. gui/src/listPage.test.tsx renders ListPage with renderToStaticMarkup, using the page() helper at lines 55-74.

Possible solution

  1. Add a pure helper to gui/src/sortModel.ts, right after shownPlayRequest. Pass the random function in as a parameter so a test can control it:
    /** Play the shown rows that have a URI, in a random order; null when none has one. */
    export function shufflePlayRequest(
      shown: TrackInfo[],
      random: () => number = Math.random,
    ): Action | null {
      const uris = shown.flatMap((track) => (track.uri ? [track.uri] : []));
      if (uris.length === 0) return null;
      for (let i = uris.length - 1; i > 0; i--) {
        const j = Math.floor(random() * (i + 1));
        [uris[i], uris[j]] = [uris[j], uris[i]];
      }
      return { PlayUris: { uris, offset: 0 } };
    }
    
  2. In ListPage.tsx, import it and wire up the button. Give it a label that names the list, the same way Play does:
    <button
      type="button"
      className="list-shuffle"
      aria-label={`Shuffle ${title}`}
      disabled={first < 0}
      onClick={() => {
        const action = shufflePlayRequest(shownTracks);
        if (action) send(action);
      }}
    >
    
  3. Add a line to CHANGELOG.md, under ## [Unreleased] > ### Added, in the same style as the entries already there:
    - **The Shuffle button on Liked Songs and playlist pages works**: in the desktop app and the browser GUI, it plays the songs the page shows in a random order, only the matching ones when you filter.
    
    That section changes often, so if it conflicts, rebase on main and keep both entries.

Traps:

  • Don't send SetShuffle or ToggleShuffle. They act on whatever is playing when you click, not on the list you're about to start. Say a Local Files song is playing and you shuffle a Spotify playlist. They would flip the Local queue's shuffle and leave Spotify alone. They would also change the shuffle setting the user picked in the player bar. All we want is a random start order, so the shuffled PlayUris does the whole job.
  • This button isn't a toggle. It doesn't get aria-pressed. The player bar's Shuffle (gui/src/Transport.tsx, lines 20-29) is the toggle, and its label is just "Shuffle". That's why this one says Shuffle ${title} (for example "Shuffle Liked Songs"): a screen reader user can then tell the two apart.
  • Don't use listPlayRequest or PlayTrackInContext. A playlist context plays in the playlist's own order, so only the first song would be random.
  • Call Math.random only inside the click handler. Don't shuffle during render or in a useMemo. The react-hooks lint rules flag impure calls in render, and the order would stay frozen until the list changed.
  • Don't shuffle with sort(() => Math.random() - 0.5). It's biased, and you can't test it with an injected random function.
  • Don't send LoadMore before shuffling, and don't wait for hasMore to turn false. spotatui already fetches the rest of the list in the background after the first page.
  • Leave the 100-URI cap in playback.rs alone. On Spotify, a list of more than 100 songs plays 100 random ones. Play has the same limit after a sort or filter.
  • If shuffle is already on in the player, the player may reorder the list again on its side. That's fine, because the result is still random.
  • An Apple Music list plays one random song, because start_playback_uris (transport.rs, lines 470-478) plays one Music track at a time. Play does the same, so leave it.
  • Don't touch anything outside gui/src and CHANGELOG.md. That means no Rust, no gui/src/bindings/ (those files are generated), and no Transport.tsx.

Tests to add:

  • In gui/src/sortModel.test.ts, add shufflePlayRequest to the import list at the top (lines 3-13), next to shownPlayRequest. Then add a describe("shufflePlayRequest", ...) block. With () => 0, every step swaps with the first slot. With () => 0.999, no step swaps anything. Both orders are easy to work out by hand:
    describe("shufflePlayRequest", () => {
      const shown = [tracks[0], track("gone", [], "", 1, null), tracks[1], tracks[2]];
    
      it("plays every shown song with a URI once, in the order the random picks give", () => {
        expect(shufflePlayRequest(shown, () => 0)).toEqual({
          PlayUris: {
            uris: ["spotify:track:Apple", "spotify:track:cherry", "spotify:track:banana"],
            offset: 0,
          },
        });
        expect(shufflePlayRequest(shown, () => 0.999)).toEqual({
          PlayUris: {
            uris: ["spotify:track:banana", "spotify:track:Apple", "spotify:track:cherry"],
            offset: 0,
          },
        });
      });
    
      it("sends nothing when no shown row has a URI", () => {
        expect(shufflePlayRequest([track("gone", [], "", 1, null)], () => 0)).toBeNull();
        expect(shufflePlayRequest([], () => 0)).toBeNull();
      });
    });
    
    Some of these lines are longer than prettier allows. Run the prettier step in "Done when" and it will reflow them for you.
  • In gui/src/listPage.test.tsx, check that the button is enabled on a loaded list and disabled while a playlist is still loading. Pull out the button tag and look for disabled:
    const shuffleButton = (html: string) =>
      html.match(/<button[^>]*aria-label="Shuffle [^"]*"[^>]*>/)?.[0] ?? "";
    
    it("enables Shuffle once the list shows a playable song", () => {
      const button = shuffleButton(page({ page: "liked" }));
      expect(button).toContain('aria-label="Shuffle Liked Songs"');
      expect(button).not.toContain("disabled");
    });
    
    For the disabled case, render the playlist directly with table={{ ...table, uri: null, tracks: [], positions: [] }}, as the "ListPage reload" test does at lines 122-148. Then expect disabled in the tag.
  • Name tests as behavior sentences, like the ones already in these files.
  • Leave tools/gates.count alone. Page tests need no counter change.

Done when

  • Run these in gui/. First run npx prettier --write . to format your changes (the gate only checks formatting, it doesn't fix it). Then run the same checks .github/workflows/gui.yml runs. They should all pass:
    npm ci
    npm run lint
    npm run format:check
    npm run typecheck
    npm test
    npm run build
    
  • To run only your tests, use npm test -- sortModel and npm test -- listPage. Check that the output shows your test ran; a filter that matches nothing runs nothing.
  • Manual check without Spotify Premium, using Local Files (optional):
    1. On Linux, install the prerequisites in docs/installation.md (Building from Source > Prerequisites) first.
    2. In gui/, run npm run build. The GUI binary embeds gui/dist, so run this again after every page change.
    3. From the repo root, run:
      cargo run --no-default-features --features telemetry,gui,streaming,discord-rpc,self-update,scripting,mcp-server,mpris,local-files,subsonic,internet-radio,youtube,qobuz --bin spotatui-gui
      
      The page opens in your browser.
    4. Pick Local Files, either in the first-launch questions or with the Source select in Your Library. Local Files reads the behavior.local_music_path setting, which defaults to your OS Music folder. To use a different folder, change it on the GUI Settings page (in the Sources section) or in config.yml. A folder with 10 or more songs works best.
    5. In Your Library, open a folder and click Shuffle a few times. Each click should usually start a different song, and the queue drawer should show the rest in a new order. Type something in the Filter box and shuffle again. Only matching songs should play.
  • Screenshots without any music source: after a one-time npx playwright install chromium firefox webkit, run npm run shots in gui/. It renders the page against the fake bridge in gui/e2e and writes PNGs to gui/shots/<browser>/. Every scene that shows a list page should now have a Shuffle icon that isn't greyed out: library-playlist.png, list-sort-filter.png and track-menu.png. The shots aren't compared against a baseline, so nothing fails there. The fake bridge also doesn't record what the page sends, so the unit tests above are what prove the request is right.
  • Attach library-playlist.png or a short recording of a shuffle to your PR.
  • CHANGELOG.md has the new line under ### Added.
  • You don't change any Rust files, so the gate needs no cargo command. CI's cargo legs still run and should pass unchanged.
Lingua principale
Rust
Stelle
1.4k
Fork
238
Merge medio
9h 19m
PR unite (30g)
186

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di LargeModGames/spotatui

Tutte le issue di LargeModGames/spotatui

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.