srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/screens/lyricsScreen.js
blob: b669891b9c9d5ac78e67ff4e2eec3e9090547dc5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { CLOSE_ICON } from './icons.js';
import { logoLockup } from '../logo.js';
import { renderCornerRail } from '../cornerRail.js';
import { attachTooltips } from '../tooltip.js';
import { renderTopRail } from '../topRail.js';
import { api, apiUrl, ApiError } from '../api.js';
import { getUser } from '../auth.js';
import { parseCustomContent } from '../customText.js';
import { escapeHtml } from '../util.js';

const POLL_INTERVAL_MS = 4000;

// Connect Spotify -> poll what's currently playing -> fetch synced lyrics
// for it -> hand the result to onLyricsReady, which app.js implements by
// setting it as the active custom text and starting a game exactly like a
// manually-uploaded .lrc file would - this screen only produces that same
// {name, chunks, language, timed} shape, it doesn't play anything itself.
export function renderLyricsScreen(root, { onBack, onLyricsReady, onShowStats, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore }) {
    let status = 'checking';
    let message = 'Checking sign-in...';
    let lastTrackKey = null;
    let pollTimer = null;
    let stopped = false;

    function bodyText() {
        if (status === 'signed-out') return 'Sign in first - the Spotify connection is tied to your account.';
        if (status === 'not-connected') return 'Connect your Spotify account to type along with whatever you\'re playing.';
        if (status === 'not-configured') return message;
        return message;
    }

    function render() {
        root.innerHTML = `
            <div class="stats-screen">
                <button class="screen-close" data-action="menu" aria-label="Close" data-tooltip="Close (Esc)">${CLOSE_ICON}</button>
                ${logoLockup()}
                <h2>Lyrics</h2>
                <div class="stats-placeholder">${escapeHtml(bodyText())}</div>
                ${status === 'signed-out' ? `<button class="menu-button" data-action="go-account">Sign In</button>` : ''}
                ${status === 'not-connected' ? `<a class="menu-button" href="${apiUrl('/api/spotify/login')}">Connect Spotify</a>` : ''}
            </div>
        `;
        root.querySelectorAll('[data-action="menu"]').forEach(el => el.addEventListener('click', () => { stopped = true; onBack(); }));
        attachTooltips(root);
        const cleanupTheme = renderTopRail(root, { onShowAccount, onShowFriends });
        const goAccount = root.querySelector('[data-action="go-account"]');
        if (goAccount) goAccount.addEventListener('click', () => { stopped = true; onShowAccount(); });
        const cleanupRail = renderCornerRail(root, { onShowStats, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore });
        return () => { cleanupTheme(); cleanupRail(); };
    }

    let cleanup = render();
    function rerender() {
        if (stopped) return;
        cleanup();
        cleanup = render();
    }

    async function poll() {
        if (stopped) return;
        if (!getUser()) {
            status = 'signed-out';
            rerender();
            return;
        }
        try {
            const playing = await api.get('/api/spotify/now-playing');
            if (!playing.is_playing || !playing.track) {
                status = 'waiting';
                message = 'Play something on Spotify to get started.';
                rerender();
            } else {
                const trackKey = `${playing.artist}::${playing.track}`;
                status = 'waiting';
                message = `Now playing: ${playing.track} - ${playing.artist}. Looking for lyrics...`;
                rerender();
                if (trackKey !== lastTrackKey) {
                    lastTrackKey = trackKey;
                    await fetchLyrics(playing);
                }
            }
        } catch (err) {
            if (err instanceof ApiError && err.status === 404) {
                status = 'not-connected';
            } else if (err instanceof ApiError && err.status === 401) {
                status = 'signed-out';
            } else if (err instanceof ApiError && err.status === 501) {
                status = 'not-configured';
                message = err.message;
            } else {
                status = 'waiting';
                message = 'Could not reach Spotify - retrying...';
            }
            rerender();
        }
        if (!stopped && status !== 'not-configured') {
            pollTimer = setTimeout(poll, POLL_INTERVAL_MS);
        }
    }

    async function fetchLyrics(playing) {
        try {
            const durationSeconds = playing.duration_ms ? Math.round(playing.duration_ms / 1000) : undefined;
            const query = new URLSearchParams({ artist: playing.artist, track: playing.track });
            if (durationSeconds) query.set('duration', String(durationSeconds));
            const result = await api.get(`/api/lyrics?${query.toString()}`);
            const raw = result.lrc || result.plain;
            const filename = result.lrc ? 'lyrics.lrc' : 'lyrics.txt';
            const { chunks, language, timed } = parseCustomContent(raw, filename);
            if (chunks.length === 0) return;
            stopped = true;
            if (pollTimer) clearTimeout(pollTimer);
            onLyricsReady({ name: `${playing.track} - ${playing.artist}`, chunks, language, timed });
        } catch (err) {
            if (err instanceof ApiError && err.status === 404) {
                message = `No lyrics found for "${playing.track}" - try a different song.`;
                rerender();
            }
        }
    }

    poll();

    return () => {
        stopped = true;
        if (pollTimer) clearTimeout(pollTimer);
        cleanup();
    };
}