refactor: backend-only transpose, remove client-side transpose logic

This commit is contained in:
2026-07-11 22:30:59 +02:00
parent 381f273f10
commit af328deac1
5 changed files with 27 additions and 53 deletions

View File

@@ -94,9 +94,16 @@ export async function listSongs(q = "", sort = "date", order = "desc"): Promise<
return res.json();
}
export async function getSong(id: string, applyCapo = false): Promise<Song | null> {
const url = applyCapo
? `${API_BASE}/songs/${id}?apply_capo=true`
export async function getSong(
id: string,
opts: { applyCapo?: boolean; transpose?: number } = {},
): Promise<Song | null> {
const params = new URLSearchParams();
if (opts.applyCapo) params.set("apply_capo", "true");
if (opts.transpose && opts.transpose !== 0)
params.set("transpose", String(opts.transpose));
const url = params.size
? `${API_BASE}/songs/${id}?${params}`
: `${API_BASE}/songs/${id}`;
const res = await fetch(url);
if (res.status === 404) return null;

View File

@@ -1,34 +0,0 @@
import type { Song } from "./types";
const NOTES_SHARP = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const NOTES_FLAT = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"];
function transposeChord(chord: string, semitones: number): string {
const match = chord.match(/^([A-G][#b]?)(.*)/);
if (!match) return chord;
const [, root, descriptor] = match;
const idx = NOTES_SHARP.indexOf(root) !== -1
? NOTES_SHARP.indexOf(root)
: NOTES_FLAT.indexOf(root);
if (idx === -1) return chord;
const newIdx = ((idx + semitones) % 12 + 12) % 12;
const notes = semitones >= 0 ? NOTES_SHARP : NOTES_FLAT;
return notes[newIdx] + descriptor;
}
export function transposeSong(song: Song, semitones: number): Song {
if (semitones === 0) return song;
return {
...song,
sections: song.sections.map((section) => ({
...section,
lines: section.lines.map((line) => ({
...line,
chords: line.chords.map((cp) => ({
...cp,
chord: transposeChord(cp.chord, semitones),
})),
})),
})),
};
}

View File

@@ -11,7 +11,6 @@ import { EditSongSheet } from "~/components/edit-song-sheet";
import { DeleteSongDialog } from "~/components/delete-song-dialog";
import { SectionNav } from "~/components/section-nav";
import { AutoScrollControls } from "~/components/auto-scroll-controls";
import { transposeSong } from "~/lib/transpose";
import { extractUniqueChords } from "~/lib/song-utils";
import { getSong } from "~/lib/api";
import { useAuth } from "~/lib/auth";
@@ -82,23 +81,19 @@ export default function SongDetail() {
useEffect(() => {
setLoading(true);
getSong(id)
getSong(id, { transpose: initOffset })
.then((s) => {
setBaseSong(s);
setDisplayedSong(s);
})
.finally(() => setLoading(false));
}, [id]);
}, [id]); // eslint-disable-line
useEffect(() => {
if (applyCapo && baseSong?.meta.capo) {
getSong(id, true).then((s) => {
if (s) setDisplayedSong(s);
});
} else {
setDisplayedSong(baseSong);
}
}, [applyCapo]); // eslint-disable-line
getSong(id, { applyCapo, transpose: offset }).then((s) => {
if (s) setDisplayedSong(s);
});
}, [id, applyCapo, offset]);
function handleOffsetChange(newOffset: number) {
setOffset(newOffset);
@@ -158,9 +153,8 @@ export default function SongDetail() {
);
}
const displayed = transposeSong(displayedSong, offset);
const uniqueChords = extractUniqueChords(displayed.sections);
const sectionItems = displayed.sections.map((s, i) => ({
const uniqueChords = extractUniqueChords(displayedSong.sections);
const sectionItems = displayedSong.sections.map((s, i) => ({
label: s.label,
index: i,
}));
@@ -200,7 +194,7 @@ export default function SongDetail() {
>
<div className="max-w-lg mx-auto lg:max-w-none">
<ChordChart
sections={displayed.sections}
sections={displayedSong.sections}
fontSize={fontSize}
onChordClick={handleChordClick}
/>
@@ -224,7 +218,6 @@ export default function SongDetail() {
</div>
</div>
{/* Bottom toolbar: section nav + auto-scroll */}
<div className="lg:hidden border-t border-border bg-background flex items-center">
<SectionNav sections={sectionItems} onJump={handleSectionJump} />
<AutoScrollControls scrollRef={scrollRef} />

View File

@@ -29,6 +29,7 @@ pub struct UpdateSongRequest {
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct GetSongQuery {
pub apply_capo: Option<bool>,
pub transpose: Option<i8>,
}
#[derive(Deserialize, ToSchema)]

View File

@@ -155,6 +155,13 @@ pub async fn get_song(
song
};
let song = match params.transpose {
Some(semitones) if semitones != 0 => {
ChordTransposer.transpose_song(&song, semitones)
}
_ => song,
};
Ok(Json(song))
}