aboutsummaryrefslogtreecommitdiffstats
path: root/web-project/src/solo_tool_web.py
blob: 7b36b7577cac3eb8aecea54c8ef7e12c57e7bdcc (plain)
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import sys
from os import getenv
from os.path import basename, splitext
from functools import partial
from nicegui import ui, events
from starlette.formparsers import MultiPartParser
import click

from solo_tool import SoloTool
from solo_tool.session_manager import getSessionManager
from solo_tool import handlers

def fileName(path: str) -> str:
    return basename(splitext(path)[0])

@ui.refreshable
def keyPointList(st: SoloTool) -> None:
    with ui.list().props('separator'):
        if st.keyPoints is not None:
            for kp in st.keyPoints:
                ui.item(f"{kp:0.2}", on_click=handlers.keyPointAbsolute(st, kp)).props('clickable v-ripple').classes('text-lg')

@ui.refreshable
def songList(st: SoloTool, songDrawer) -> None:
    with ui.list().props('separator'):
        for i, path in enumerate(st.songs):
            ui.item(fileName(path), on_click=handlers.songAbsolute(st, i, lambda: songDrawer.hide())).props('clickable v-ripple')

sessions = {}
sessionManager = None

@ui.page('/{sessionName}')
def sessionPage(sessionName: str):
    if sessionName not in sessions:
        return

    fullscreen = ui.fullscreen()
    ui.dark_mode().enable()
    ui.colors(secondary='#ffc107')
    ui.page_title(sessionName)

    # Improved performance with large file uploads
    MultiPartParser.max_file_size = 1024 * 1024 * 100  # 100 MB

    st = sessions[sessionName]

    # Upload song dialog
    def handleFileUpload(e: events.UploadEventArguments):
        sessionManager.addSong(e.name, e.content)
        st.addSong(e.name)

    with ui.dialog() as uploadSongDialog:
        ui.upload(label="Upload songs", auto_upload=True, on_upload=handleFileUpload).classes('max-w-full')

    # Header
    with ui.header().classes('items-center justify-between'):
        with ui.row().classes('items-center justify-start'):
            ui.button(icon='menu', on_click=lambda: songDrawer.toggle()).props('flat dense round color=white')
            ui.label().bind_text_from(st, 'song', lambda index: fileName(st.songs[index]) if index is not None else "Select a song").classes('text-lg')
        with ui.row().classes('items-center justify-start'):
            ui.button(icon='home', on_click=lambda: ui.navigate.to("/")).props('flat dense round color=white')
            def save(): sessionManager.saveSession(st, sessionName)
            ui.button(icon='save', on_click=save).props('flat dense round color=white')
            ui.button(icon='fullscreen', on_click=fullscreen.toggle).props('flat dense round color=white')

    # Key points list
    with ui.right_drawer(top_corner=True, bottom_corner=True).props('width=120 behavior=desktop'):
        ui.label("Key Points").classes('text-lg')
        keyPointList(st)
        def addKeyPoint() -> None: st.keyPoints += [st.keyPoint]
        ui.button(icon='add', on_click=addKeyPoint).props('flat round dense color=white')

    # Song list
    with ui.left_drawer(bottom_corner=True).props('overlay breakpoint=8000') as songDrawer:
        songList(st, songDrawer)
        ui.button(icon='add', on_click=uploadSongDialog.open).props('flat round dense color=white')

    # Playback position
    def setPosition(e) -> None: st.position = e.args
    ui.slider(min=0, max=1.0, step=0.001) \
            .bind_value_from(st, 'position') \
            .on('change', setPosition) \
            .props('thumb-size=0px track-size=16px')

    # Key point position
    ui.slider(min=0, max=1.0, step=0.001).bind_value(st, 'keyPoint').props('selection-color=transparent color=secondary')

    # Play control
    with ui.button_group().classes('w-full').style('height: 80px'):
        buttonSize = "20px"
        ui.button(icon='skip_previous', on_click=handlers.restartOrPreviousSong(st, 0.01)).props(f"size={buttonSize}").style('flex: 1')
        ui.button(color='positive', on_click=handlers.playPause(st)).bind_icon_from(st, "playing", lambda playing: "pause" if playing else "play_arrow").props(f"size={buttonSize}").style('flex: 1')
        ui.button(icon='vertical_align_bottom', on_click=handlers.positionToKeyPoint(st), color='negative').props(f"size={buttonSize}").style('flex: 2')
        ui.button(icon='undo', on_click=st.jump, color='secondary').props(f"size={buttonSize}").style('flex: 2')
        ui.button(icon='skip_next', on_click=handlers.songRelative(st, 1)).props(f"size={buttonSize}").style('flex: 1')

    # Playback rate
    with ui.row().classes('w-full justify-between no-wrap items-center'):
        markerLabels = ",".join([f"{v}:'{v}x'" for v in [0.4, 0.6, 0.8, 1.0, 1.2]])
        ui.slider(min=0.4, max=1.2, step=0.05).bind_value(st, 'rate').props(f'snap markers :marker-labels="{{{markerLabels}}}"').classes('q-px-md')

    # Volume
    with ui.row().classes('w-full justify-between no-wrap items-center'):
        volumeLabels = ",".join([f"{v}:'{int(v*100)}%'" for v in [0.0, 0.25, 0.5, 0.75, 1.0, 1.25]])
        ui.slider(min=0, max=1.25, step=0.01).bind_value(st, 'volume').props(f':marker-labels="{{{volumeLabels}}}"').classes('q-px-md')

@ui.page('/')
def landingPage():
    ui.dark_mode().enable()
    ui.page_title("Solo Tool")

    # Header
    with ui.header().classes('items-center'):
        ui.label("Choose a session").classes('text-lg')

    for name, soloTool in sessions.items():
        ui.button(name, on_click=partial(ui.navigate.to, f"/{name}"))

def start(port, refresh, reload, session_path, song_path):
    global sessionManager
    sessionManager = getSessionManager(song_path, session_path)

    for key in sessionManager.getSessions():
        songTool = sessionManager.loadSession(key)
        songTool.registerKeyPointListCallback(lambda new: keyPointList.refresh())
        songTool.registerSongSelectionCallback(lambda new: keyPointList.refresh())
        songTool.registerSongListCallback(lambda new: songList.refresh())
        sessions[fileName(key)] = songTool

    try:
        ui.run(reload=reload, binding_refresh_interval=refresh, port=port)
    except KeyboardInterrupt:
        pass

@click.command()
@click.option("--port", type=int, default=8080, help="Port on which to bind.")
@click.option("--refresh", type=float, default=0.5, help="Refresh interval in seconds.")
@click.option("--reload/--no-reload", default=True, help="Auto-reload when files change.")
@click.option("--session_path", default="/home/eddy/music/sessions", help="Look for sessions in this location.")
@click.option("--song_path", default="/home/eddy/music/songs", help="Look for songs in this location.")
def main(port, refresh, reload, session_path, song_path):
    start(port, refresh, reload, session_path, song_path)

# Hardcoded dev settings
if __name__ in {"__main__", "__mp_main__"}:
    start(8080, 0.5, True, "/home/eddy/music/sessions", "/home/eddy/music/songs")