Skip to content

Commit b4fd786

Browse files
authored
Merge pull request #459 from bohning/webserver
Webserver
2 parents 0f8f6ca + 6d3f249 commit b4fd786

23 files changed

Lines changed: 1370 additions & 406 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## Features
66

7+
- A webserver can be launched to let devices in the local network browse your song collection.
78
- Added filters for custom data to the sidebar tree.
89
- Files may be deleted permanently instead of being sent to the trash folder - useful to save disk space or if trashing is not supported on your system.
910
- Medley sections are now checked if they start on a line start and end on a line end, otherwise a warning is issued.

poetry.lock

Lines changed: 614 additions & 387 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ attrs = "*"
2424
beautifulsoup4 = "*"
2525
ffmpeg-normalize = ">=1.27"
2626
filetype = "*"
27+
flask = "^3.1.1"
2728
keyring = "*"
2829
lxml = "*"
2930
mutagen = "*"
@@ -32,6 +33,7 @@ packaging = "*"
3233
pillow = ">=10"
3334
platformdirs = "*"
3435
PySide6 = "*"
36+
qrcode = {extras = ["pil"], version = "^8.2"}
3537
reportlab = "*"
3638
requests = "*"
3739
rookiepy = "^0.5.3"

src/usdb_syncer/db/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,17 @@ def delete_sync_metas(ids: tuple[SyncMetaId, ...]) -> None:
845845
)
846846

847847

848+
def delete_sync_metas_in_folder(folder: Path, ids: tuple[SyncMetaId, ...]) -> None:
849+
path = folder.as_posix()
850+
for batch in batched(ids, _SQL_VARIABLES_LIMIT - 1):
851+
id_str = ", ".join("?" for _ in range(len(batch)))
852+
_DbState.connection().execute(
853+
f"DELETE FROM sync_meta WHERE sync_meta_id IN ({id_str}) AND "
854+
"path GLOB ? || '/*'",
855+
(*batch, path),
856+
)
857+
858+
848859
@attrs.define(frozen=True, slots=False)
849860
class CustomMetaDataParams:
850861
"""Parameters for inserting or updating a resource file."""

src/usdb_syncer/gui/__init__.py

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import subprocess
88
import sys
9+
import time
910
import traceback
1011
from argparse import ArgumentParser
1112
from collections.abc import Callable
@@ -18,7 +19,16 @@
1819
from PySide6.QtCore import Qt
1920

2021
import usdb_syncer
21-
from usdb_syncer import addons, db, errors, logger, settings, song_routines, utils
22+
from usdb_syncer import (
23+
addons,
24+
db,
25+
errors,
26+
logger,
27+
settings,
28+
song_routines,
29+
utils,
30+
webserver,
31+
)
2232
from usdb_syncer import sync_meta as sync_meta
2333
from usdb_syncer import usdb_song as usdb_song
2434
from usdb_syncer.gui import events, hooks, theme
@@ -40,6 +50,7 @@ class CliArgs:
4050
"""Command line arguments."""
4151

4252
reset_settings: bool = False
53+
subcommand: str = ""
4354

4455
# Settings
4556
songpath: Path | None = None
@@ -49,9 +60,14 @@ class CliArgs:
4960
skip_pyside: bool = not utils.IS_SOURCE
5061
trace_sql: bool = False
5162

52-
# Subcommands
63+
# preview
5364
txt: Path | None = None
5465

66+
# webserver
67+
host: str | None = None
68+
port: int | None = None
69+
title: str | None = None
70+
5571
@classmethod
5672
def parse(cls) -> CliArgs:
5773
parser = ArgumentParser(description="USDB Syncer")
@@ -86,11 +102,28 @@ def parse(cls) -> CliArgs:
86102
)
87103

88104
subcommands = parser.add_subparsers(
89-
title="subcommands", description="Subcommands."
105+
title="subcommands", description="Subcommands.", dest="subcommand"
90106
)
91107
preview = subcommands.add_parser("preview", help="Show preview for song txt.")
92108
preview.add_argument("txt", type=Path, help="Path to the song txt file.")
93109

110+
serve = subcommands.add_parser(
111+
"serve", help="Launch webserver with local songs."
112+
)
113+
serve.add_argument(
114+
"--host",
115+
type=int,
116+
help="Host for the webservice. Default is the device's public IP address. "
117+
"Use 127.0.0.1 (localhost) to not be accessible by other devies "
118+
"on the local network.",
119+
)
120+
serve.add_argument(
121+
"--port",
122+
type=int,
123+
help="Port the webservice will bind to. Defaults to a random free port.",
124+
)
125+
serve.add_argument("--title", help="Title displayed at the top of the page.")
126+
94127
return parser.parse_args(namespace=cls())
95128

96129
def apply(self) -> None:
@@ -113,26 +146,36 @@ def main() -> None:
113146
utils.AppPaths.make_dirs()
114147
app = _init_app()
115148
app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, False)
116-
if args.txt:
117-
if not _run_preview(args.txt):
149+
match args.subcommand:
150+
case "preview":
151+
if not args.txt or not _run_preview(args.txt):
152+
return
153+
case "serve":
154+
_run_webserver(host=args.host, port=args.port, title=args.title)
118155
return
119-
else:
120-
if args.profile:
121-
_with_profile(_run_main)
122-
else:
123-
_run_main()
156+
case _:
157+
if args.profile:
158+
_with_profile(_run_main)
159+
else:
160+
_run_main()
124161
app.exec()
125162

126163

164+
def configure_logging(mw: MainWindow | None = None) -> None:
165+
handlers: list[logging.Handler] = [
166+
logging.FileHandler(utils.AppPaths.log, encoding="utf-8"),
167+
logging.StreamHandler(sys.stdout),
168+
]
169+
if mw:
170+
handlers.append(_TextEditLogger(mw))
171+
logger.configure_logging(*handlers)
172+
173+
127174
def _run_main() -> None:
128175
from usdb_syncer.gui.mw import MainWindow
129176

130177
mw = MainWindow()
131-
logger.configure_logging(
132-
logging.FileHandler(utils.AppPaths.log, encoding="utf-8"),
133-
logging.StreamHandler(sys.stdout),
134-
_TextEditLogger(mw),
135-
)
178+
configure_logging(mw)
136179
mw.label_update_hint.setVisible(False)
137180
if not utils.IS_SOURCE:
138181
if version := utils.newer_version_available():
@@ -153,12 +196,26 @@ def _run_main() -> None:
153196

154197

155198
def _run_preview(txt: Path) -> bool:
199+
configure_logging()
156200
from usdb_syncer.gui.previewer import Previewer
157201

158202
theme.Theme.from_settings().apply()
159203
return Previewer.load_txt(txt)
160204

161205

206+
def _run_webserver(
207+
host: str | None = None, port: int | None = None, title: str | None = None
208+
) -> None:
209+
configure_logging()
210+
webserver.start(host=host, port=port, title=title)
211+
logger.logger.info("Webserver is running in headless mode. Press Ctrl+C to stop.")
212+
try:
213+
while True:
214+
time.sleep(1)
215+
except KeyboardInterrupt:
216+
webserver.stop()
217+
218+
162219
def _excepthook(
163220
error_type: type[BaseException], error: BaseException, tb_type: TracebackType | None
164221
) -> Any:

src/usdb_syncer/gui/forms/MainWindow.ui

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
<addaction name="action_settings"/>
211211
<addaction name="action_meta_tags"/>
212212
<addaction name="action_generate_song_list"/>
213+
<addaction name="action_webserver"/>
213214
</widget>
214215
<widget class="QMenu" name="menu_usdb">
215216
<property name="title">
@@ -858,6 +859,15 @@
858859
<string>Tune Perfect</string>
859860
</property>
860861
</action>
862+
<action name="action_webserver">
863+
<property name="icon">
864+
<iconset resource="../resources/qt/resources.qrc">
865+
<normaloff>:/icons/server-black.svg</normaloff>:/icons/server-black.svg</iconset>
866+
</property>
867+
<property name="text">
868+
<string>Webserver</string>
869+
</property>
870+
</action>
861871
</widget>
862872
<resources>
863873
<include location="../resources/qt/resources.qrc"/>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Dialog</class>
4+
<widget class="QDialog" name="Dialog">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>348</width>
10+
<height>322</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Webserver</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QLabel" name="label_status">
19+
<property name="text">
20+
<string>The webserver is not currently running.</string>
21+
</property>
22+
<property name="openExternalLinks">
23+
<bool>true</bool>
24+
</property>
25+
</widget>
26+
</item>
27+
<item>
28+
<layout class="QFormLayout" name="formLayout">
29+
<item row="2" column="0">
30+
<widget class="QLabel" name="label_2">
31+
<property name="text">
32+
<string>Port:</string>
33+
</property>
34+
</widget>
35+
</item>
36+
<item row="2" column="1">
37+
<widget class="QSpinBox" name="box_port">
38+
<property name="sizePolicy">
39+
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
40+
<horstretch>0</horstretch>
41+
<verstretch>0</verstretch>
42+
</sizepolicy>
43+
</property>
44+
<property name="alignment">
45+
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
46+
</property>
47+
<property name="maximum">
48+
<number>65535</number>
49+
</property>
50+
</widget>
51+
</item>
52+
<item row="1" column="0">
53+
<widget class="QLabel" name="label">
54+
<property name="text">
55+
<string>Title:</string>
56+
</property>
57+
</widget>
58+
</item>
59+
<item row="1" column="1">
60+
<widget class="QLineEdit" name="edit_title"/>
61+
</item>
62+
</layout>
63+
</item>
64+
<item>
65+
<spacer name="verticalSpacer_2">
66+
<property name="orientation">
67+
<enum>Qt::Orientation::Vertical</enum>
68+
</property>
69+
<property name="sizeHint" stdset="0">
70+
<size>
71+
<width>20</width>
72+
<height>40</height>
73+
</size>
74+
</property>
75+
</spacer>
76+
</item>
77+
<item>
78+
<widget class="QLabel" name="label_qrcode">
79+
<property name="minimumSize">
80+
<size>
81+
<width>150</width>
82+
<height>150</height>
83+
</size>
84+
</property>
85+
<property name="text">
86+
<string/>
87+
</property>
88+
<property name="alignment">
89+
<set>Qt::AlignmentFlag::AlignCenter</set>
90+
</property>
91+
</widget>
92+
</item>
93+
<item>
94+
<spacer name="verticalSpacer">
95+
<property name="orientation">
96+
<enum>Qt::Orientation::Vertical</enum>
97+
</property>
98+
<property name="sizeHint" stdset="0">
99+
<size>
100+
<width>20</width>
101+
<height>40</height>
102+
</size>
103+
</property>
104+
</spacer>
105+
</item>
106+
<item>
107+
<layout class="QHBoxLayout" name="horizontalLayout">
108+
<item>
109+
<widget class="QPushButton" name="button_start">
110+
<property name="text">
111+
<string>Start</string>
112+
</property>
113+
</widget>
114+
</item>
115+
<item>
116+
<widget class="QPushButton" name="button_stop">
117+
<property name="text">
118+
<string>Stop</string>
119+
</property>
120+
</widget>
121+
</item>
122+
</layout>
123+
</item>
124+
</layout>
125+
</widget>
126+
<resources/>
127+
<connections/>
128+
</ui>

src/usdb_syncer/gui/icons.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Icon(enum.Enum):
7171
REPORT = "report.png", "notebook-white.svg"
7272
SAFARI = "safari.png", None
7373
SAVED_SEARCH = "heart.png", "heart-white.svg"
74+
SERVER = "server-black.svg", "server-white.svg"
7475
SETTINGS = "cog.png", "cog-white.svg"
7576
SKIP_BACKWARD = "control-double-180.png", "skip-previous-white.svg"
7677
SKIP_FORWARD = "control-double.png", "skip-next-white.svg"

0 commit comments

Comments
 (0)