Compare commits
3
Commits
f5008b8d8e
...
v0.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dbfe26c1f | ||
|
|
0345d948ac | ||
|
|
51ff98eff1 |
No files matched your search
@@ -59,14 +59,33 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
KUBECONFIG: ${{ runner.temp }}/k3s-kubeconfig
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
kubectl port-forward --namespace alisten service/alisten-serve 18090:9090 &
|
||||
|
||||
kubectl port-forward \
|
||||
--namespace alisten \
|
||||
--address 127.0.0.1 \
|
||||
service/alisten-serve 18090:9090 &
|
||||
pid=$!
|
||||
trap 'kill "$pid" 2>/dev/null || true' EXIT
|
||||
|
||||
base_url="http://127.0.0.1:18090"
|
||||
|
||||
curl --silent --show-error --fail \
|
||||
--retry 10 \
|
||||
--retry-delay 2 \
|
||||
--connect-timeout 2 \
|
||||
--max-time 5 \
|
||||
--retry 20 \
|
||||
--retry-delay 1 \
|
||||
--retry-connrefused \
|
||||
--max-time 10 \
|
||||
http://127.0.0.1:18090/healthz
|
||||
--retry-max-time 60 \
|
||||
"${base_url}/readyz"
|
||||
|
||||
expected="${TAG}+${HEAD_SHA}"
|
||||
actual="$(curl --silent --show-error --fail \
|
||||
--connect-timeout 2 \
|
||||
--max-time 5 \
|
||||
"${base_url}/version")"
|
||||
|
||||
diff -u --label expected --label actual \
|
||||
<(echo "$expected") <(echo "$actual")
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func NewHandler(ready func() bool, metrics prometheus.Gatherer) http.Handler {
|
||||
func NewHandler(ready func() bool, metrics prometheus.Gatherer, version string) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, "ok\n")
|
||||
@@ -20,6 +20,9 @@ func NewHandler(ready func() bool, metrics prometheus.Gatherer) http.Handler {
|
||||
}
|
||||
_, _ = io.WriteString(w, "ok\n")
|
||||
})
|
||||
mux.HandleFunc("GET /version", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, version+"\n")
|
||||
})
|
||||
mux.Handle("GET /metrics", promhttp.HandlerFor(metrics, promhttp.HandlerOpts{}))
|
||||
return mux
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func TestHandler(t *testing.T) {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(prometheus.NewGauge(prometheus.GaugeOpts{Name: "test_gauge", Help: "测试用指标。"}))
|
||||
var ready atomic.Bool
|
||||
handler := NewHandler(ready.Load, registry)
|
||||
handler := NewHandler(ready.Load, registry, "v1.2.3+abc")
|
||||
|
||||
get := func(path string) *httptest.ResponseRecorder {
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -28,6 +28,12 @@ func TestHandler(t *testing.T) {
|
||||
t.Errorf("/healthz 状态码 = %d,期望 200", rec.Code)
|
||||
}
|
||||
})
|
||||
t.Run("version 返回构建版本", func(t *testing.T) {
|
||||
rec := get("/version")
|
||||
if body := rec.Body.String(); rec.Code != http.StatusOK || body != "v1.2.3+abc\n" {
|
||||
t.Errorf("/version 状态码 = %d,响应 = %q,期望 200 和 \"v1.2.3+abc\\n\"", rec.Code, body)
|
||||
}
|
||||
})
|
||||
t.Run("metrics 输出注册的指标", func(t *testing.T) {
|
||||
rec := get("/metrics")
|
||||
body, _ := io.ReadAll(rec.Body)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestHealthcheckFollowsReadiness(t *testing.T) {
|
||||
var ready atomic.Bool
|
||||
server := httptest.NewServer(ops.NewHandler(ready.Load, prometheus.NewRegistry()))
|
||||
server := httptest.NewServer(ops.NewHandler(ready.Load, prometheus.NewRegistry(), version))
|
||||
defer server.Close()
|
||||
t.Setenv("ALISTEN_OPS_LISTEN", server.Listener.Addr().String())
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ func runServe(base *slog.Logger) (err error) {
|
||||
|
||||
opsLogger := base.With("component", "ops")
|
||||
publicServer := newHTTPServer(p.handler, base.With("component", "http"))
|
||||
opsServer := newHTTPServer(recoverPanics(ops.NewHandler(ready, metrics), opsLogger), opsLogger)
|
||||
opsServer := newHTTPServer(recoverPanics(ops.NewHandler(ready, metrics, version), opsLogger), opsLogger)
|
||||
go func() { fatal <- fmt.Errorf("HTTP 服务异常退出:%w", publicServer.Serve(publicListener)) }()
|
||||
go func() { fatal <- fmt.Errorf("HTTP 服务异常退出:%w", opsServer.Serve(opsListener)) }()
|
||||
logger.Info("服务已启动", "listen", publicListener.Addr().String(), "ops_listen", opsListener.Addr().String(), "instance", instance)
|
||||
|
||||
@@ -275,7 +275,7 @@ func (w mediaWorker) run(ctx context.Context, listener net.Listener) error {
|
||||
}),
|
||||
)
|
||||
opsLogger := w.base.With("component", "ops")
|
||||
opsServer := newHTTPServer(recoverPanics(ops.NewHandler(ready, w.metrics), opsLogger), opsLogger)
|
||||
opsServer := newHTTPServer(recoverPanics(ops.NewHandler(ready, w.metrics, version), opsLogger), opsLogger)
|
||||
go func() { fatal <- fmt.Errorf("运维端口异常退出:%w", opsServer.Serve(listener)) }()
|
||||
w.logger.Info("worker 已启动", "ops_listen", listener.Addr().String(), "instance", w.instance)
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
:root { --detail-panel-width: 420px; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.app-shell > .player-bar { justify-self: center; margin-bottom: 12px; }
|
||||
.app-shell > .player-bar { margin: 0 8px 8px; }
|
||||
}
|
||||
/* 通知相对窗口居中,侧栏打开时移到内容区中间,否则会盖住侧栏。窗口太窄时内容区放不下通知,留在原位。 */
|
||||
@media (min-width: 1024px) {
|
||||
|
||||
@@ -15,16 +15,17 @@
|
||||
.player-tools { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
||||
.player-tools .volume-control { display: flex; align-items: center; gap: 6px; margin-right: 16px; }
|
||||
.player-tools .volume-slider { width: 90px; accent-color: var(--color-foreground); }
|
||||
.player-tools .volume-input { box-sizing: border-box; width: 4.5ch; min-width: 0; padding: 4px 3px; text-align: center; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
|
||||
.player-tools .volume-input::-webkit-inner-spin-button, .player-tools .volume-input::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; }
|
||||
.player-tools .volume-unit { color: var(--color-foreground-subtle); font-size: var(--text-ui-xs); }
|
||||
.player-tools .volume-readout { min-width: 5ch; color: var(--color-foreground-subtle); font-size: var(--text-ui-sm); font-variant-numeric: tabular-nums; }
|
||||
.player-panel-toggle { color: var(--color-foreground-subtle); }
|
||||
.player-panel-toggle:hover:not(:disabled), .player-panel-toggle:focus-visible:not(:disabled), .player-panel-toggle[aria-pressed='true'] { color: var(--color-brand); background: transparent; }
|
||||
.player-panel-toggle:active:not(:disabled) { color: var(--color-brand); background: transparent; transform: none; }
|
||||
/* 播放栏在布局里占一行,不压在页面上,所以各页面不用留出底部空间。 */
|
||||
@media (min-width: 768px) {
|
||||
.player-bar { --color-hover: var(--color-surface-hover); width: min(920px, calc(100% - 16px)); padding: 10px 14px; border-radius: 16px; background: var(--color-panel); box-shadow: 0 8px 24px var(--color-shadow); }
|
||||
.player-bar { --color-hover: var(--color-surface-hover); padding: 10px 14px; border-radius: 12px; background: var(--color-panel); }
|
||||
.player-artwork .artwork { width: 56px; background: var(--color-surface); }
|
||||
.player-right .icon-button { width: 40px; height: 40px; }
|
||||
.player-right .icon-button svg { width: 22px; height: 22px; }
|
||||
.player-center .icon-button svg { width: 24px; height: 24px; }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.player-bar { gap: 12px; }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { roomStore } from '@/core/room'
|
||||
import { sourceName } from '@/features/queue'
|
||||
import { formatDuration } from '@/shared/lib/time'
|
||||
import { useStore } from '@/shared/lib/use-store'
|
||||
import { Artwork, AsyncIconButton, IconButton, VolumeInput } from '@/shared/ui'
|
||||
import { Artwork, AsyncIconButton, IconButton } from '@/shared/ui'
|
||||
import { playerStore } from './player-store'
|
||||
import { volumeStore } from './volume-store'
|
||||
import './playback.css'
|
||||
@@ -64,6 +64,7 @@ export function PlayerBar({ onSkip, onLyrics, onQueue, onChat, queueOpen, chatOp
|
||||
const connected = useStore(roomStore, s => s.status === 'open')
|
||||
const volume = useStore(volumeStore, s => s.volume)
|
||||
const muted = useStore(volumeStore, s => s.muted)
|
||||
const audibleVolume = muted ? 0 : volume
|
||||
const meta = nowPlaying?.entry.meta
|
||||
const artist = meta?.artist || '未知艺术家'
|
||||
const source = nowPlaying ? sourceName(nowPlaying.entry.track) : undefined
|
||||
@@ -90,15 +91,23 @@ export function PlayerBar({ onSkip, onLyrics, onQueue, onChat, queueOpen, chatOp
|
||||
</div>
|
||||
<div className="player-tools">
|
||||
<IconButton icon={muted ? VolumeX : Volume2} label={muted ? '取消静音' : '静音'} onClick={() => volumeStore.setMuted(!muted)} />
|
||||
<VolumeInput
|
||||
volume={volume}
|
||||
sliderValue={muted ? 0 : volume}
|
||||
onCommit={(next) => {
|
||||
volumeStore.setVolume(next)
|
||||
if (muted && next > 0)
|
||||
volumeStore.setMuted(false)
|
||||
}}
|
||||
/>
|
||||
<div className="volume-control">
|
||||
<input
|
||||
className="volume-slider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={audibleVolume}
|
||||
aria-label="音量"
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value)
|
||||
volumeStore.setVolume(next)
|
||||
if (muted && next > 0)
|
||||
volumeStore.setMuted(false)
|
||||
}}
|
||||
/>
|
||||
<span className="volume-readout" aria-hidden="true">{`${audibleVolume}%`}</span>
|
||||
</div>
|
||||
<IconButton icon={MessageCircle} label="房间聊天" className="player-panel-toggle" disabled={!canOpenPanels} aria-pressed={chatOpen} onClick={onChat} />
|
||||
<IconButton icon={ListMusic} label="播放列表" className="player-panel-toggle" disabled={!canOpenPanels} aria-pressed={queueOpen} onClick={onQueue} />
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,11 @@ import { errorMessage } from '@/core/errors'
|
||||
import { historyStore, searchLimits } from '@/features/history'
|
||||
import { reminderStore, requestNotificationPermission, systemNotificationsSupported, volumeStore } from '@/features/playback'
|
||||
import { useStore } from '@/shared/lib/use-store'
|
||||
import { notificationStore, Select, VolumeInput } from '@/shared/ui'
|
||||
import { notificationStore, Select } from '@/shared/ui'
|
||||
import { appearanceStore, fontSizes, themes } from './appearance-store'
|
||||
import { performanceLevels, performanceStore } from './performance-store'
|
||||
import { profileStore } from './profile-store'
|
||||
import { VolumeInput } from './volume-input'
|
||||
import './settings.css'
|
||||
|
||||
interface SettingsDraft {
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -4,13 +4,12 @@ import { beginVolumeEdit, changeVolumeDraft, commitVolumeDraft, initialVolumeEdi
|
||||
interface VolumeInputProps {
|
||||
volume: number
|
||||
onCommit: (volume: number) => void
|
||||
sliderValue?: number
|
||||
id?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function VolumeInput({ volume, onCommit, sliderValue, id, className = '', disabled = false }: VolumeInputProps) {
|
||||
export function VolumeInput({ volume, onCommit, id, className = '', disabled = false }: VolumeInputProps) {
|
||||
const [editor, setEditor] = useState(() => initialVolumeEditor(volume))
|
||||
const syncedEditor = syncVolumeDraft(editor, volume)
|
||||
const commit = () => {
|
||||
@@ -27,7 +26,7 @@ export function VolumeInput({ volume, onCommit, sliderValue, id, className = '',
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={sliderValue ?? volume}
|
||||
value={volume}
|
||||
aria-label="音量"
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
@@ -16,4 +16,3 @@ export { Skeleton } from './skeleton'
|
||||
export { SongArtwork } from './song-artwork'
|
||||
export { SongList } from './song-list'
|
||||
export type { SongAction, SongItem } from './song-list'
|
||||
export { VolumeInput } from './volume-input'
|
||||
Reference in new issue
Block a user