Compare commits

...
Sign in to create a new pull request.

264 commits

Author SHA1 Message Date
cd84e90583 Also add infos if client reconnects 2025-02-20 16:39:17 +01:00
0a16feb5d5 Push queue info directly at registration to playback client 2025-02-20 15:52:36 +01:00
3eda77012d Broadcast state even when playback client is disconnected 2025-02-19 22:26:32 +01:00
7fd54527c8 Added simple queue view in player 2025-02-18 09:53:40 +01:00
8eb484abc2 Better error handling on startup when NameError occurs 2025-02-18 09:48:22 +01:00
15206453ab Include Exception in typings 2025-02-17 18:34:36 +01:00
b3faae7b10 Handle disconnect more gracefully 2025-02-17 18:33:48 +01:00
a761126eb5 Added a section for the web client in the README 2025-02-17 16:25:10 +01:00
5ae1b2d7d9 Rebranding Windows install to "Syng.Rocks! Karaoke Player" 2025-02-17 16:13:45 +01:00
1d4ae2de84 Make desktop shortcut optional 2025-02-16 15:33:10 +01:00
6db66b1488 Install wix extension globally 2025-02-16 12:51:09 +01:00
0165ecd6f0 Typo in workflow file 2025-02-16 12:45:04 +01:00
33d12db622 Explicitly set wix version 2025-02-16 12:35:18 +01:00
ebc8dd18f5 Add agpl to installer and install extension in GH actions 2025-02-16 12:28:04 +01:00
dc7b2052ba Added AGPL License in rtf for installer 2025-02-16 11:05:12 +01:00
56da2ffb1b Install the extensions in container 2025-02-16 00:25:19 +01:00
66b4f5de95 Added UI extension to wix call 2025-02-16 00:14:20 +01:00
3fab9c0402 add gui to installer 2025-02-16 00:09:18 +01:00
a5be76b752 Update position of generated msi file 2025-02-15 21:37:08 +01:00
6672b3d7cd Getting closer 2025-02-15 21:30:42 +01:00
b954a37a04 I wish I could do this in a branch ... 2025-02-15 21:23:59 +01:00
837339df6c I don't think this will work, but hey 2025-02-15 21:20:00 +01:00
ff5b20bf0d I think I slowly understand how this is meant to be done 2025-02-15 21:00:23 +01:00
3623af341f Why is there no good documentation/tutorial for this? 2025-02-15 20:51:42 +01:00
bea36bcd14 Maybe this works? 2025-02-15 20:43:52 +01:00
d7021a934a Move wxs file to dist folder 2025-02-15 20:38:28 +01:00
0f675a22ad Add correct source directory for data\** 2025-02-15 20:29:18 +01:00
9ca8df948c More testing how wix works 2025-02-15 20:11:40 +01:00
8fc4b2c30c I know how XML works, I am a professional 😅 2025-02-15 20:05:00 +01:00
3acb64e6aa Since this is only run on GH Actions, this will take a bit of trial and
error
2025-02-15 19:59:06 +01:00
3c78ff5ad4 Testing building msi with wix 2025-02-15 19:48:16 +01:00
9e0b41505c fixed building installer dir 2025-02-15 18:06:22 +01:00
bb4607e198 Better naming for GH artifacts 2025-02-15 17:56:37 +01:00
d247707c11 Show advanced now ignores itself.
Honestly, how did this ever work?
2025-02-15 17:53:49 +01:00
5688fef47d First steps towards install on windows 2025-02-15 17:32:09 +01:00
48c02426e0
Merge pull request #10 from christofsteel/installer
Seperate docker and windows workflows
2025-02-15 17:13:29 +01:00
e3f7f4d471 Changed requirements-client to requirements in windows action 2025-02-15 16:52:45 +01:00
53d6e6efda Seperate docker and windows workflows 2025-02-15 16:48:05 +01:00
284eb8c6b7 Improved logging
* added logging level critical
 * critical messages will be displayed in a popup
2025-02-15 16:35:07 +01:00
0265ddda71 Improved disconnect messaging 2025-02-15 13:47:31 +01:00
607e117325 Update README 2025-02-15 12:11:10 +01:00
c3c0176697 Update dependencies 2025-02-15 12:06:44 +01:00
c2f2682cce Dependency alt-profanity-check is now optional due.
If it is present, it is used otherwise no profanity check is performed.
This is due to the complexity of installing alt-profanity-check and only
affects the server.
2025-02-15 11:34:31 +01:00
2ae5c7ac06 some automated reformatting 2025-02-15 11:29:03 +01:00
421392e1a8 Removed dedicated save button for config. Renamed the "Save and Start" button to "Connect" 2025-02-15 02:03:34 +01:00
bf48104acb Removed old code, that was commented out. 2025-02-15 01:44:23 +01:00
6d56fd7def Added max_duration option to youtube source 2025-02-15 01:44:23 +01:00
19c2db9a23 Buffering log message should now only appear once for each entry 2025-02-15 01:44:23 +01:00
bd7928a8ca Better handling of error message 2025-02-15 01:44:23 +01:00
eb479df689 Treat playback session as admin session on the server 2025-02-15 01:44:23 +01:00
8828733271 Removed option to build index. Filebased sources should always have an index 2025-02-15 01:44:23 +01:00
c381133252 Added is_valid check to sources. Filebased entries are valid, iff they appear in index. 2025-02-15 01:44:23 +01:00
9232f3870f Added function on the server side, to send logs directly to the playbackclient 2025-02-15 01:44:23 +01:00
8550387881 Added dedicated log tab 2025-02-15 01:44:23 +01:00
6975aefb23 Removed unused failed flag in DLEntry 2025-02-15 01:44:23 +01:00
57fab12f4e Check if entry is valid before returning 2025-02-15 01:44:23 +01:00
91dec5bcd9 Explicitly set QT_API to pyqt6 for qasync. Fixes #8 2025-02-13 12:48:45 +01:00
25cf546843 Initialize empty config when run via CLI, fixes #7 2025-02-13 11:01:06 +01:00
dcab27bf96 Changed logging label to logging textedit 2025-02-06 20:03:53 +01:00
391562fd3a Allow the suffix for youtube searches to be configurable. Fixes #3 2025-02-02 23:48:45 +01:00
0df93ac758 Add method apply_config to sources 2025-02-02 23:47:04 +01:00
de74d9ecd7 Specify if config options should be send to the server 2025-02-02 23:45:47 +01:00
dfbecc1517 Configure indexing of source 2025-02-02 23:44:30 +01:00
7488b260e1 MPV changed something, so audio_file does not work anymore :/
Audio is now loaded via a callback
2025-02-02 14:10:52 +01:00
1dcdb8d405 Put version checking a custom function 2025-02-02 12:28:35 +01:00
bdc057026f Update dependencies 2025-02-02 12:11:28 +01:00
7b30100cb7 return -> return None
to satisfy mypy
2025-02-01 00:50:49 +01:00
56c200fa3a Explicitly annotate loglines 2025-02-01 00:50:34 +01:00
cc873fc4ff check compatible versions on connect (playback client) 2025-01-31 22:17:29 +01:00
979e1d385e Multiline logs in gui 2025-01-31 22:16:24 +01:00
02ec988f90 Searching and adding to the queue now emits an ID to the client 2025-01-31 16:42:10 +01:00
55a3685c76 Added typing for AsyncServer.instrument() 2025-01-01 13:05:13 +01:00
935add9144 Add option for socketio admin ui 2025-01-01 13:03:06 +01:00
fdcae7edab removed a line of debug code 2024-12-29 00:14:09 +01:00
dec88d0641 Forgot to reactivare docker builds 2024-11-21 19:33:18 +01:00
e4c5f84c00 Update README.md 2024-11-21 18:29:08 +00:00
d769ed9168 Updated README.md 2024-11-21 19:21:23 +01:00
d2109204f8 removed some copy and paste leftovers 2024-11-21 16:56:09 +01:00
d4af3c0afd Added screenshots to readme and flatpak and version bump 2024-11-21 15:58:53 +01:00
94e0d9c0b7 web screenshots 2024-11-21 15:35:10 +01:00
e7172a610f New screenshots 2024-11-21 15:30:09 +01:00
02716a7723 Updated README and documentation and fixed mix up between left and right :/ 2024-11-21 14:30:48 +01:00
948bb4da5c Added configuration for the QR code 2024-11-21 14:16:13 +01:00
9b5b1ee9d0 Update build-and-publish.yaml 2024-11-21 12:34:02 +00:00
6231d1f47c Testing automatic windows build for libmpv 2024-11-21 12:50:47 +01:00
3315f95a4d Deleted accidental commited build file 2024-11-21 12:37:17 +01:00
c6c91d050d Added flatpak version bump 2024-11-19 13:06:57 +01:00
00d85f429b Locked new versions 2024-11-19 12:53:18 +01:00
7fc5b26391 Removed imports 2024-11-18 22:05:04 +01:00
3fb43de576 Better handling of buffering and start_streaming 2024-11-18 22:04:31 +01:00
220f1e8779 Added windows support for libmpv 2024-11-18 19:02:34 +01:00
55bedf7aa3 cleaning up, preparing for windows tests 2024-11-18 18:15:13 +01:00
e7b895888b Update deps 2024-11-18 17:05:21 +01:00
d21c747927 dependencies are non optional 2024-11-18 17:00:42 +01:00
65a749b5f8 Added some types 2024-11-18 17:00:42 +01:00
95e8157277 Client starting and stopping from gui 2024-11-18 17:00:42 +01:00
089de1ff93 PyQt now also uses asyncio and its thread contains the main loop 2024-11-18 17:00:42 +01:00
bf2e854cdd Closing the mpv window now disconnects the client 2024-11-18 17:00:42 +01:00
d4cf649735 Skipping playback now works reliably 2024-11-18 17:00:42 +01:00
824198acf6 skipping is more consistent, but still does not handle multiple rapid skips (edgecase) 2024-11-18 17:00:42 +01:00
f446733bb2 simplified external audio loading 2024-11-18 17:00:42 +01:00
63555dde87 most things work, only skipping song needs work 2024-11-18 17:00:42 +01:00
e51acd075a Getting closer 2024-11-18 17:00:42 +01:00
15cc8f8147 Working in a buggy state... but more or less working 2024-11-18 17:00:40 +01:00
fc9b79172a play entries and static images 2024-11-18 16:55:54 +01:00
b931c4d916 Initial libmpv interface 2024-11-18 16:55:43 +01:00
4320ecd560 Version 2.0.7 2024-11-18 13:48:01 +01:00
b37dd9f75b Fixed ytsearch a bit more and fixed missing metainfo for direct yt links 2024-11-18 13:42:09 +01:00
838f91d480 Fixed broken search for youtube (hopefully) 2024-11-18 13:20:24 +01:00
8144c772f9 Version 2.0.6 2024-11-18 12:16:37 +01:00
f0d070aff6 set yt-dlp dependencies back to latest 2024-11-18 12:10:38 +01:00
dd12de4a7c Downgrading yt-dlp and yarl dependencies due to problems 2024-11-16 11:33:02 +01:00
4a8d54e7ba updated flatpak metainfo to 2.0.4 2024-11-15 22:41:19 +01:00
7ef3eb60f5 Update version and dependencies 2024-11-15 22:37:06 +01:00
96463a8edb even smaller screenshots 2024-11-15 21:35:42 +00:00
3a0b360769 Scale screenshots 2024-11-15 21:31:26 +00:00
dd348c834a Added screenshots to README 2024-11-15 21:28:39 +00:00
a99679eda5 Fixed a bug that could deadlock the player 2024-11-15 22:20:49 +01:00
02e77662ac update syng-web 2024-11-15 22:13:35 +01:00
fdf72ac4f7 removed __cache__-thingy, that was only around to hold data from search results to forming the entries... 2024-10-14 18:26:59 +02:00
cd3e6d6d7c decorators for server 2024-10-14 15:32:06 +02:00
8900251b1e put server in an own class 2024-10-14 13:02:12 +02:00
3f6113dab1 (Server) Added moving entries to arbitrary positions
(web) Updated web, implemented drag and drop
2024-10-13 15:43:29 +02:00
a134a07bff Version 2.0.3 2024-10-11 09:27:42 +02:00
388d577d0e Updated flatpak metainfo 2024-10-11 09:21:04 +02:00
c571e82105 Updated web-ui
fixes waiting-room
implemented proper kiosk mode
2024-10-11 01:11:07 +02:00
0f3a005792 More meaningful error if gui is not available 2024-10-10 23:45:43 +02:00
6030dc7ff6 Fixed Cannot open file '' error 2024-10-09 17:33:05 +02:00
f73eb20955 Fixed python version to 3.12 for mypy and ruff checking.
It seems, that scipy is not compatible with 3.13 yet
2024-10-09 16:22:18 +02:00
fb12bdedd8 Moved away from this awful global client construction.
Client is now a class, that can be instantiated and contains its state.
2024-10-09 16:17:55 +02:00
f2e04ab95e Update README.md 2024-10-08 13:11:15 +00:00
d1700ba75c Updated flatpak metainfo 2024-10-06 21:13:36 +02:00
3a537760d3 Update screenshots 2024-10-06 21:07:25 +02:00
e4204cee29 Add visibility toggle icons for password fields 2024-10-06 21:06:03 +02:00
402e8a854d fixed metainfos xml 2024-10-06 19:03:57 +02:00
7d608d99f2 Versionbump to 2.0.2 2024-10-06 17:21:37 +02:00
b16520c555 Windows Icons broken again... fixed (hopefully) 2024-10-06 16:28:02 +02:00
806ee45ec2 Update workflow 2024-10-06 16:07:06 +02:00
d09f81f4e3 Server documentation includes now docker 2024-10-06 15:58:07 +02:00
51c6e75522 Updated docker tagging 2024-10-06 15:57:50 +02:00
a9524b5cf4 Automatically build and publish docker files (hopefully) 2024-10-06 15:24:25 +02:00
230fc58a06 Update README.md 2024-10-06 12:57:18 +00:00
aeb41b2eae made mypy and ruff happy 2024-10-06 14:55:27 +02:00
528cc357f2 Updated tagline 2024-10-06 14:53:53 +02:00
18322412c2 Add Dockerfile 2024-10-06 14:51:23 +02:00
a60cc1922d make chunk enumerating start at 0 2024-10-06 14:51:01 +02:00
b12276eb7c Added server parts 2024-10-06 14:20:51 +02:00
ee85aaa46a Update index files in the background 2024-10-06 14:19:05 +02:00
e4155140f1 Fixed local index file generation 2024-10-06 13:50:08 +02:00
f3ecc11eda Fixed typing 2024-10-06 04:01:25 +02:00
3a2d6e43e0 Better shrink "Simple" GUI 2024-10-06 04:00:38 +02:00
9395980ecb Simplified summaries and descriptions 2024-10-06 04:00:23 +02:00
e3265b0817 Fixed types and imports 2024-10-06 02:39:04 +02:00
c78a48bd10 Added Import/Export/Clear-Config buttons, hide advanced options by default 2024-10-06 02:30:23 +02:00
eb725c7c33 Reworked Config Schemas, added File/Folder-Picker and dedicated Int-Spinners 2024-10-06 02:29:40 +02:00
9279a6a5a2 Added custom mpv options 2024-10-06 02:24:19 +02:00
73ab2896f9 Log connection errors 2024-10-06 02:20:58 +02:00
b7809c94b8 Added custom hide/unhide password buttons 2024-10-06 02:20:27 +02:00
dd17c9a91b Added windows release builder 2024-10-05 20:11:56 +02:00
2588ec9735 option to manually trigger the workflow 2024-10-05 15:01:25 +02:00
002f25b60b testing github action with/on windows 2024-10-05 14:48:46 +02:00
18745d18e2 update dependencies 2024-10-05 14:30:19 +02:00
7b50882738 Finally got windows icon to work 2024-10-02 17:14:02 +02:00
829f7b26d1 Console output borked on windows, fixed 2024-10-02 16:15:24 +02:00
1d99945f44 Windows icon 2024-10-01 17:01:09 +02:00
5a75893aa3 Included instructions for the windows version 2024-10-01 16:44:03 +02:00
07cd3c1f96 add multiprocessing freeze support for pyinstaller for windows 2024-10-01 14:16:48 +02:00
9a2cfac637 Added initial windows support 2024-10-01 12:32:11 +02:00
bbbf54c9ed updated metainfo (ul outside of p) 2024-09-30 17:58:10 +02:00
e2895a287c Fixed typing in client 2024-09-30 15:54:32 +02:00
b960b4365b Version bump 2024-09-30 15:50:49 +02:00
6557823d83 updated flatpak build workflow 2024-09-30 15:50:29 +02:00
ea6a5d40ad Some local improvements to flatpak-pip-generator 2024-09-30 15:49:08 +02:00
dd84ff361b forward all logging to gui, if it exists 2024-09-30 14:39:29 +02:00
46f68e2e92 create cache directory, if not exists 2024-09-30 14:39:13 +02:00
fb05b44b71 updated python dependencies 2024-09-30 14:38:48 +02:00
225a605438 Updated description 2024-09-30 14:37:31 +02:00
14f9dcb702 Update README.md 2024-09-30 09:20:22 +00:00
60c3450b3c Update README.md 2024-09-30 09:19:39 +00:00
2c2c818bfc Reference the flathub version 2024-09-30 11:17:13 +02:00
e08d108b0b reference a specific commit for screenshots 2024-09-29 14:47:38 +02:00
6472ef4b5a Update README.md 2024-09-26 17:11:57 +00:00
312fe16cbb Datei .gitlab-ci.yml aktualisieren 2024-09-26 17:05:22 +00:00
e2686f19bf Datei .gitlab-ci.yml aktualisieren 2024-09-26 16:57:09 +00:00
152a8ecd0e Datei .gitlab-ci.yml aktualisieren 2024-09-26 16:52:04 +00:00
ab83ef900e Datei .gitlab-ci.yml aktualisieren 2024-09-26 16:49:17 +00:00
26024819d6 One of these days, I will get markdown right the first time around... one of these days... 2024-09-26 15:46:52 +00:00
0be608ccde CI testing was assuming old layouts 2024-09-26 17:36:57 +02:00
06b400061c Fancy badges do not want to be centered :/ 2024-09-26 17:32:40 +02:00
ab4f808296 Added fancy shields to readme 2024-09-26 17:29:56 +02:00
4ea76e8787 updated commit hash 2024-09-24 00:08:50 +02:00
58b3837d1b organized flatpak dependencies 2024-09-23 23:43:02 +02:00
1cd547a895 remove icon from metainfo 2024-09-23 23:38:44 +02:00
b22f64505e reorganizing flatpak builder 2024-09-23 19:55:42 +02:00
fdb5310c60 removed mirror url from git sources in mpv building 2024-09-23 19:55:20 +02:00
ea234e6d11 Version 2.0.0 🎉 2024-09-23 19:10:37 +02:00
6501cff7df New screenshot 2024-09-23 19:08:14 +02:00
3c69e22bc5 Prerelease version bump 2024-09-23 18:49:15 +02:00
997f7b418c updated webui 2024-09-23 18:16:36 +02:00
9bc76795cf added dist to gitignore 2024-09-23 18:16:25 +02:00
09a9cd34ae added metainfos to pyproject.toml 2024-09-23 17:54:34 +02:00
f70d02f75b youtube -> YouTube in description 2024-09-23 17:50:33 +02:00
4ad4563d80 set version to prerelease, and deleted rest of starting server in gui 2024-09-23 17:49:16 +02:00
27976b05d8 Updated readme to prepare for 2.0 release 2024-09-23 17:36:28 +02:00
10239e42f5 removed starting local server from gui, I never liked it anyway 2024-09-23 17:35:53 +02:00
d1f7de5bb7 removed url from metainfo and added developer 2024-09-23 17:35:35 +02:00
3ab15bf04e Renamed metadata.xml -> metainfo.xml 2024-09-23 17:15:06 +02:00
391075faaf Implemented password fields in GUI 2024-09-23 17:14:08 +02:00
b9aa39e5b7 updated flatpak metainfo 2024-09-23 17:13:48 +02:00
68f0d0a1f8 Screenshot of web ui 2024-09-23 14:27:16 +02:00
4e77018d7b add screenshot of config gui 2024-09-23 14:23:04 +02:00
4fb3942ce9 readd brotlicff to flatpak 2024-09-23 14:22:50 +02:00
3cb781194d update yt-dlp in mpv 2024-09-23 14:22:26 +02:00
d3b0b56d57 Move to only yaml flatpak configs 2024-09-23 14:22:04 +02:00
af242a9c33 build flatplak from local files 2024-09-23 09:27:47 +02:00
2aac11a54e Added autogenerated flatpak metadata 2024-09-23 09:10:21 +02:00
c18c6a152e remove old flatpak files 2024-09-23 03:55:07 +02:00
efa30022fe add new flatpak files 2024-09-23 03:54:44 +02:00
c934a9c4e9 missing flatpak files 2024-09-23 01:11:14 +02:00
9a7ebeb22e initial flatpak files 2024-09-23 01:11:14 +02:00
Christoph Stahl
ff17a6dc7f Removed old dependencies, added classifiers 2024-09-23 01:09:14 +02:00
de762c170a Removed old typings 2024-09-23 00:26:55 +02:00
b5759b1aa2 removed unused code 2024-09-23 00:19:17 +02:00
8ef77beaea renamed dist to resources 2024-09-23 00:17:10 +02:00
2779ab991a weird error, maybe bug... this fixed it 2024-09-23 00:15:44 +02:00
Christoph Stahl
5468b39bc1 Communication between GUI and client back to multiprocessing, since Popen yielded 100% CPU :/ 2024-09-22 23:34:30 +02:00
Christoph Stahl
1f1c2c4f1e Add yt search results from client search to YouTube Cache 2024-09-22 22:02:34 +02:00
Christoph Stahl
bf104362ea Hotfix for yt restricted search 2024-09-22 21:23:06 +02:00
Christoph Stahl
b80f6559a7 better handling starting parameters 2024-09-22 21:21:53 +02:00
Christoph Stahl
2d3313e734 fix headless server mode 2024-09-22 20:33:57 +02:00
Christoph Stahl
20654960cc Rewrote README.md 2024-09-22 19:48:52 +02:00
Christoph Stahl
07bb35e704 mypy and ruff 2024-09-22 19:02:19 +02:00
Christoph Stahl
d33497d09c removed last of tk, communication between gui and client 2024-09-22 18:59:02 +02:00
Christoph Stahl
fdec53a884 yt source is enabled by default 2024-09-22 18:58:32 +02:00
Christoph Stahl
3f68f657bf cache files are now stored in user dir 2024-09-22 18:58:21 +02:00
Christoph Stahl
0a36f657e2 switched default server to syng.rocks 2024-09-22 18:57:55 +02:00
Christoph Stahl
4717d94bdc updated requirements 2024-09-22 18:57:38 +02:00
Christoph Stahl
bb8fe8304e updated icons and added other resources 2024-09-22 18:56:18 +02:00
Christoph Stahl
ffa8927859 Removed debug prints in server 2024-09-22 13:05:02 +02:00
Christoph Stahl
50585463fc removed tk and added qt gui 2024-09-22 13:02:09 +02:00
Christoph Stahl
7689172494 update dependencies 2024-09-22 12:58:46 +02:00
Christoph Stahl
85961cf238 removed ideas for gtk gui 2024-09-22 12:58:27 +02:00
b0b763495f Finally figured out how to add icons in gnome :/ 2024-09-20 16:57:19 +02:00
4ce5df4103 Reworked tk gui in preparation for migration to qt 2024-09-19 18:26:25 +02:00
e2655a483b Removed syng-client, syng-server and syng-gui in favor of syng <subcommand> 2024-09-19 14:25:46 +02:00
6a84567545 Reworked dev dependencies in pyproject.toml 2024-09-19 00:04:44 +02:00
da9ef35ba4 Implemented restricted mode and client side search querying.
Also lots of documentation
2024-09-18 23:59:29 +02:00
bcb8843b35 Added pylint as dev dependancy 2024-09-18 23:58:46 +02:00
c78169d669 Added typed-pyyaml to dev dependency 2024-09-15 22:28:33 +02:00
411ccdd2c9 black -> line-length=100 2024-07-16 14:52:04 +02:00
f4e15908cc reenabling extension filter for s3 2024-07-11 00:20:40 +02:00
14498110ae Unified start script to syng 2024-07-11 00:15:54 +02:00
1d6e1b8dc8 Datei .gitlab-ci.yml aktualisieren 2024-07-09 22:11:33 +00:00
ca16dff23b Fixed some typing errors 2024-07-09 21:25:52 +02:00
705169a1f7 missing metadata for youtube 2024-07-09 20:49:24 +02:00
b46d5175cd Increased minimal python version 2024-07-09 20:13:31 +02:00
d2d316078b Implemented a profanity checker for performer names 2024-06-19 11:02:08 +02:00
60b0fd42c2 Some typings, to improve compatibility with pyright 2024-06-18 02:23:19 +02:00
725beab857 Youtube: renamed attributes to be compatible with constructor 2024-06-17 17:18:45 +02:00
ae630b7236 indentation fail :/ 2024-06-17 17:08:10 +02:00
e5a2f88e0b Type annotations for cache 2024-06-17 17:04:59 +02:00
55939887f3 cache metadata for search results... TODO at some point they need to be removed, but not today... 2024-06-17 17:04:10 +02:00
56ab58586a completely removed pytube. All YT communication is now done via yt-dlp. 2024-06-17 16:13:03 +02:00
aac8314837 Fixed typing in s3 2024-06-17 16:10:27 +02:00
81 changed files with 14289 additions and 3964 deletions

123
.github/workflows/build-and-publish.yaml vendored Normal file
View file

@ -0,0 +1,123 @@
name: Build for windows and docker and create a release
# Controls when the workflow will run
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build-windows:
# The type of runner that the job will run on
runs-on: windows-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
repository: christofsteel/syng
- name: Install 7-Zip
run: choco install -y 7zip
- name: Download and extract latest MPV nightly
run: |
Invoke-WebRequest -Uri https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20241121/mpv-dev-x86_64-20241121-git-4b11f66.7z -OutFile mpv.7z
7z x mpv.7z
- name: Download and extract FFMPEG 7.1
run: |
Invoke-WebRequest -Uri https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.1-full_build.7z -OutFile ffmpeg-release-full.7z
7z x ffmpeg-release-full.7z
- name: Populate workdir
run: |
mkdir work
Copy-Item -Recurse -Verbose syng work/syng
Copy-Item -Verbose requirements-client.txt work/requirements.txt
Copy-Item -Verbose resources/icons/syng.ico work/
Copy-Item -Verbose syng/static/background.png work/
Copy-Item -Verbose syng/static/background20perc.png work/
Copy-Item -Verbose libmpv-2.dll work/
Copy-Item -Verbose ffmpeg-7.1-full_build/bin/ffmpeg.exe work/
- uses: actions/setup-python@v5
name: Install Python
with:
python-version: 3.12
- name: Install poetry
run: pip install poetry
- name: Extract version from Poetry
id: get_version
run: echo "VERSION=$(poetry version -s)" >> $GITHUB_ENV
shell: bash
- name: Install PyInstaller
run: pip install pyinstaller
- name: Bundle Syng
run: |
pip install -r requirements.txt
pyinstaller -n "syng-${{ env.VERSION }}" -F -w -i'.\syng.ico' --add-data='.\syng.ico;.' --add-data='.\background.png;.' --add-data='.\background20perc.png;.' --add-binary '.\libmpv-2.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py
working-directory: ./work
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Syng Version ${{ env.VERSION }}
path: work/dist/syng-${{ env.VERSION }}.exe
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
repository: christofsteel/syng
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./resources/docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

65
.github/workflows/docker.yaml vendored Normal file
View file

@ -0,0 +1,65 @@
name: Build docker container
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
repository: christofsteel/syng
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./resources/docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

117
.github/workflows/windows.yaml vendored Normal file
View file

@ -0,0 +1,117 @@
name: Build for windows
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build-windows:
# The type of runner that the job will run on
runs-on: windows-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
repository: christofsteel/syng
- name: Install 7-Zip
run: choco install -y 7zip
- name: Download and extract latest MPV nightly
run: |
Invoke-WebRequest -Uri https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20250215/mpv-dev-x86_64-20250215-git-834f99e.7z -OutFile mpv.7z
7z x mpv.7z
- name: Download and extract FFMPEG 7.1
run: |
Invoke-WebRequest -Uri https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z -OutFile ffmpeg-release-full.7z
7z x ffmpeg-release-full.7z
- name: Populate workdir
run: |
mkdir work
mkdir work/portable
Copy-Item -Verbose requirements-client.txt work/requirements.txt
Copy-Item -Recurse -Verbose syng work/portable/syng
Copy-Item -Verbose resources/icons/syng.ico work/portable/
Copy-Item -Verbose syng/static/background.png work/portable/
Copy-Item -Verbose syng/static/background20perc.png work/portable/
Copy-Item -Verbose libmpv-2.dll work/portable/
Copy-Item -Verbose ffmpeg-7.1-full_build/bin/ffmpeg.exe work/portable/
mkdir work/install
Copy-Item -Recurse -Verbose syng work/install/syng
Copy-Item -Verbose requirements-client.txt work/install/requirements.txt
Copy-Item -Verbose resources/icons/syng.ico work/install/
Copy-Item -Verbose syng/static/background.png work/install/
Copy-Item -Verbose syng/static/background20perc.png work/install/
Copy-Item -Verbose libmpv-2.dll work/install/
Copy-Item -Verbose ffmpeg-7.1-full_build/bin/ffmpeg.exe work/install/
- uses: actions/setup-python@v5
name: Install Python
with:
python-version: 3.13
- name: Install poetry
run: pip install poetry
- name: Extract version from Poetry
id: get_version
run: echo "VERSION=$(poetry version -s)" >> $GITHUB_ENV
shell: bash
- name: Install PyInstaller
run: pip install pyinstaller
- name: Installing requirements
run: pip install -r requirements.txt
working-directory: ./work
# - name: Bundle Syng (portable)
# run:
# pyinstaller -n "syng-${{ env.VERSION }}" -F -w -i'.\syng.ico' --add-data='.\syng.ico;.' --add-data='.\background.png;.' --add-data='.\background20perc.png;.' --add-binary '.\libmpv-2.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py
# working-directory: ./work/portable
- name: Bundle Syng (install)
run:
pyinstaller -D --contents-directory data -w -i'.\syng.ico' --add-data='.\syng.ico;.' --add-data='.\background.png;.' --add-data='.\background20perc.png;.' --add-binary '.\libmpv-2.dll;.' --add-binary '.\ffmpeg.exe;.' -n syng syng/main.py
working-directory: ./work/install
# build msi
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Install WiX
run: |
dotnet tool install --global wix --version 5.0.2
wix extension add -g WixToolset.UI.wixext/5.0.2
- name: Copy wix file to dist
run: |
Copy-Item -Verbose resources/windows/syng.wxs work/install/dist/syng.wxs
Copy-Item -Verbose resources/windows/agpl-3.0.rtf work/install/dist/agpl-3.0.rtf
- name: Build WiX on Windows
run: wix build -ext WixToolset.UI.wixext .\syng.wxs
working-directory: ./work/install/dist
# - name: Upload artifact (portable)
# uses: actions/upload-artifact@v4
# with:
# name: Syng Version ${{ env.VERSION }} portable
# path: work/portable/dist/syng-${{ env.VERSION }}.exe
- name: Upload artifact (install)
uses: actions/upload-artifact@v4
with:
name: Syng Version ${{ env.VERSION }} Installer
path: work/install/dist/syng.msi

3
.gitignore vendored
View file

@ -1,4 +1,7 @@
docs/build docs/build
dist
__pycache__ __pycache__
.venv .venv
.idea .idea
.flatpak-builder
repo

View file

@ -1,16 +1,14 @@
image: python:3-alpine image: python:3.12
variables:
MYPYPATH: "stubs/"
mypy: mypy:
stage: test stage: test
script: script:
- pip install mypy types-Pillow types-PyYAML --quiet - pip install poetry
- mypy syng --strict - poetry install --all-extras
- poetry run mypy syng --strict
ruff: ruff:
stage: test stage: test
script: script:
- pip install ruff --quiet - pip install ruff --quiet
- ruff syng - ruff check syng

207
README.md
View file

@ -1,27 +1,206 @@
# Syng <p align="center">
<img src="https://raw.githubusercontent.com/christofsteel/syng/refs/heads/main/resources/icons/hicolor/512x512/apps/rocks.syng.Syng.png"
height="130">
Syng is an all-in-one karaoke software, consisting of a *backend server*, a *web frontend* and a *playback client*. _Easily host karaoke events_
<p align="center">
[![Matrix](https://img.shields.io/matrix/syng%3Amatrix.org?logo=matrix&label=%23syng%3Amatrix.org)](https://matrix.to/#/#syng:matrix.org)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/113266262154630635?domain=https%3A%2F%2Ffloss.social&style=flat&logo=mastodon&logoColor=white)](https://floss.social/@syng)
[![PyPI - Version](https://img.shields.io/pypi/v/syng?logo=pypi)](https://pypi.org/project/syng/)
[![Flathub Version](https://img.shields.io/flathub/v/rocks.syng.Syng?logo=flathub)](https://flathub.org/apps/rocks.syng.Syng)
[![PyPI - License](https://img.shields.io/pypi/l/syng)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![Website](https://img.shields.io/website?url=https%3A%2F%2Fsyng.rocks%2F&label=syng.rocks)](https://syng.rocks)
[![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/christofsteel%2Fsyng2?gitlab_url=https%3A%2F%2Fgit.k-fortytwo.de%2F&branch=main&logo=python&label=mypy%2Bruff)](https://git.k-fortytwo.de/christofsteel/syng2)
**Syng** is an all-in-one karaoke software, consisting of a *backend server*, a *web frontend* and a *playback client*.
Karaoke performers can search a library using the web frontend, and add songs to the queue. Karaoke performers can search a library using the web frontend, and add songs to the queue.
The playback client retrieves songs from the backend server and plays them in order. The playback client retrieves songs from the backend server and plays them in order.
Currently, songs can be accessed using the following sources: You can play songs from **YouTube**, an **S3** storage or simply share local **files**.
- **YouTube.** The backend server queries YouTube for the song and forwards the URL to the playback client. The playback client then downloads the video from YouTube for playback. The playback client uses [mpv](https://mpv.io/) for playback and can therefore play a variety of file formats, such as `mp3+cdg`, `webm`, `mp4`, ...
- **S3.** The backend server holds a list of all file paths accessible through the s3 storage, and forwards the chosen path to the playback client. The playback client then downloads the needed files from the s3 for playback.
- **Files.** Same as S3, but all files reside locally on the playback client.
The playback client uses `mpv` for playback and can therefore play a variety of file formats, such as `mp3+cdg`, `webm`, `mp4`, ... Join our [matrix room](https://matrix.to/#/#syng:matrix.org) or follow us on [mastodon](https://floss.social/@syng) for update notifications and support.
# Installation # Screenshots
<img src="https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng.png" alt="Main Window" height=200/> <img src="https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_advanced.png" alt="Main Window (Advanced)" height=200/>
## Server <img src="https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_web2.png" alt="Web Interface" height=200/> <img src="https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_mobile_search.png" alt="Web Interface on Mobile" height=200/>
pip install "syng[server] @ git+https://github.com/christofsteel/syng.git" <img src="https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_player_next_up.png" alt="Player (next up)" height=200/> <img src="https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_player_song.png" alt="Player playing a song" height=200/>
This installs the server part (`syng-server`), if you want to self-host a syng server. There is a publicly available syng instance at https://syng.rocks. # Client
## Client [![Get in on Flathub](https://flathub.org/api/badge?locale=en)](https://flathub.org/apps/rocks.syng.Syng)
pip install "syng[client] @ git+https://github.com/christofsteel/syng.git" To host a karaoke event, you only need to use the playback client. You can use the publicly available instance at https://syng.rocks as your server.
## Installation
### Linux
The preferred way to install the client is via [Flathub](https://flathub.org/apps/rocks.syng.Syng).
Alternatively Syng can be installed via the _Python Package Index_ (PyPI). When installing the client it is mandatory to include the `client` flag:
pip install 'syng[client]'
This installs both the playback client (`syng client`) and a configuration GUI (`syng gui`).
**Note:** When installing via PyPI, you need to have [mpv](https://mpv.io/) installed on the playback client, and the `mpv` binary must be in your `PATH`.
### Windows
Windows support is experimental, but you can download the current version from [Releases](https://github.com/christofsteel/syng/releases). No installation necessary, you can just run the `exe`.
## Configuration
You can host karaoke events using the default configuration. But if you need more advanced configuration, you can either configure Syng using the GUI or via a text editor by editing `~/.config/syng/config.yaml`. There are the following settings:
* `server`: URL of the server to connect to.
* `room`: The room code for your karaoke event. Can be chosen arbitrarily, but must be unique. Unused rooms will be deleted after some time. _Note:_ Everyone, that has access to the room code can join the karaoke event.
* `secret`: The admin password for your karaoke event. If you want to reconnect with a playback client to a room, these must match. Additionally, this unlocks admin capabilities to a web client, when given under "Advanced" in the web client.
* `waiting_room_policy`: One of `none`, `optional`, `forced`. When a performer wants to be added to the playback queue, but has already a song queued, they can be added to the _waiting room_. `none` disables this behavior and performers can have multiple songs in the queue, `optional` gives the performer a notification, and they can decide for themselves, and `forced` puts them in the waiting room every time. Once the current song of a performer leaves the queue, the song from the waiting room will be added to the queue.
* `last_song`: `none` or a time in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). When a song is added to the queue, and its ending time exceeds this value, it is rejected.
* `preview_duration`: Before every song, there is a short slide for the next performer. This sets how long it is shown in seconds.
* `key`: If the server, you want to connect to is in _private_ or _restricted_ mode, this will authorize the client. Private server reject unauthorized playback clients, restricted servers limit the searching to be _client only_.
* `buffer_in_advance`: How many songs should be buffered in advanced.
* `qr_box_size`: The size of one box (think pixel) of the QR Code in the playback window.
* `qr_position`: Position of the QR Code in the playback window. One of `bottom-left`, `bottom-right`, `top-left`, `top-right`.
* `show_advanced`: Show advanced options in the configuration GUI.
In addition to the general config, has its own configuration under the `sources` key of the configuration.
### YouTube
Configuration is done under `sources``youtube` with the following settings:
* `enabled`: `true` or `false`.
* `channels`: list of YouTube channels. If this is a nonempty list, Syng will only search these channels, otherwise YouTube will be searched as a whole.
* `tmp_dir`: YouTube videos will be downloaded before playback. This sets the directory, where YouTube videos are stored.
* `max_res`: Maximum resolution of a video.
* `start_streaming`: `true` or `false`. If `true`, videos will be streamed directly using `mpv`, if the video is not cached beforehand. Otherwise, Syng waits for the video to be downloaded.
* `seach_suffix`: A string that is appended to each search query. Default is "karaoke".
* `max_duration`: Maximum length of accepted videos in seconds. Default is 1800 (30 minutes)
### S3
Configuration is done under `sources``s3` with the following settings:
* `enabled`: `true` or `false`.
* `extensions`: List of extensions to be searched. For karaoke songs, that separate audio and video (e.g. CDG files), you can use `mp3+cdg` to signify, that the audio part is a `mp3` file and the video is a `cdg` file. For karaoke songs, that do not separate this (e.g. mp4 files), you can simply use `mp4`.
* `endpoint`: Endpoint of the s3.
* `access_key` Access key for the s3.
* `secret_key`: Secret key for the s3.
* `secure`: If `true` uses `ssl`, otherwise not.
* `bucket`: Bucket for the karaoke files.
* `index_file`: Cache file, that contains the filenames of the karaoke files in the s3.
* `tmp_dir`: Temporary download directory of the karaoke files.
### Files
Configuration is done under `sources``files` with the following settings:
* `enabled`: `true` or `false`.
* `extensions`: List of extensions to be searched. For karaoke songs, that separate audio and video (e.g. CDG files), you can use `mp3+cdg` to signify, that the audio part is a `mp3` file and the video is a `cdg` file. For karaoke songs, that do not separate this (e.g. mp4 files), you can simply use `mp4`.
* `dir`: Directory, where the karaoke files are stored.
### Default configuration
```
config:
key: ''
last_song: null
preview_duration: 3
room: <Random room code>
secret: <Random secret>
server: https://syng.rocks
waiting_room_policy: none
show_advanced: false
buffer_in_advance: 2
qr_box_size: 5
qr_position: bottom-right
sources:
files:
dir: .
enabled: false
extensions:
- mp3+cdg
s3:
access_key: ''
bucket: ''
enabled: false
endpoint: ''
extensions:
- mp3+cdg
index_file: ${XDG_CACHE_DIR}/syng/s3-index
secret_key: ''
secure: true
tmp_dir: ${XDG_CACHE_DIR}/syng
youtube:
channels: []
enabled: true
start_streaming: false
max_res: 720
tmp_dir: ${XDG_CACHE_DIR}/syng
search_suffix: karaoke
max_duration: 1800
```
# Web client
The web client consists of three columns on desktop and three tabs on mobile:
- **Search:** Users can search for karaoke songs and get the results here. You can also directly add a YouTube video by using its link. Search results for YouTube videos have a second button to preview the song.
- **Queue:** Shows the current queue. The current song is highlighted at the top and each item is equipped with an ETA. If you are on an admin connection, you can drag and drop to change the order of the queue and delete items from the queue.
- **Recent:** This shows all previously played songs.
When connecting to the web client, you can give yourself a name with which your songs are queued. You can change your name by changing it in the footer. If no name is selected, a name is queried each time a song is added.
In the advanced options, you can add the admin password, that corresponds with the admin password on the playback client, to elevate this connection to an admin connection.
# Server
If you want to host your own Syng server, you can do that, but you can also use the publicly available Syng instance at https://syng.rocks.
## Python Package Index
You can install the server via pip:
pip install syng
and then run via:
syng server
The server is also automatically available if you install the client.
There exists one optional dependency for the server: `alt-profanity-check`. If this package is installed, each username is checked for profanity, otherwise no such check happens.
## Docker
Alternatively you can run the server using docker. It listens on port 8080 and reads a key file at `/app/keys.txt` when configured as private or restricted.
docker run --rm -v /path/to/your/keys.txt:/app/keys.txt -p 8080:8080 ghcr.io/christofsteel/syng -H 0.0.0.0
## Configuration
Configuration is done via command line arguments, see `syng server --help` for an overview.
## Public, Restricted, Private and keys.txt
Syng can run in three modes: public, restricted and private. This restricts which playback clients can start an event and what capabilities the event has.
This has no bearing on the web clients. Every web client, that has access to the room code can join the event.
Authorization is done via an entry in the `keys.txt`
- Public means, that there are no restrictions. Every playback client can start an event and has support for all features
- Restricted means, that every playback client can start an event, but server side searching is limited to authorized clients. For unauthorized clients, a search request is forwarded to the playback client, that handles that search.
- Private means, that only authorized clients can start an event.
The `keys.txt` file is a simple text file holding one `sha256` encrypted password per line. Passwords are stored as their hex value and only the first 64 characters per line are read by the server. You can use the rest to add comments.
To add a key to the file, you can simply use `echo -n "PASSWORD" | sha256sum | cut -d ' ' -f 1 >> keys.txt`.
This installs both the playback client (`syng-client`) and a configuration GUI (`syng-gui`).

2357
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,47 +1,60 @@
[tool.poetry] [tool.poetry]
name = "syng" name = "syng"
version = "2.0.0" version = "2.1.0"
description = "" description = "Easily host karaoke events"
authors = ["Christoph Stahl <christoph.stahl@tu-dortmund.de>"] authors = ["Christoph Stahl <christoph.stahl@tu-dortmund.de>"]
license = "GPL3" license = "AGPL-3.0-or-later"
readme = "README.md" readme = "README.md"
include = ["syng/static"] include = ["syng/static"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Environment :: X11 Applications :: Qt",
"Framework :: AsyncIO",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.9",
"Topic :: Multimedia :: Sound/Audio :: Players",
"Topic :: Multimedia :: Video :: Display",
"Typing :: Typed"
]
homepage = "https://syng.rocks"
repository = "https://github.com/christofsteel/syng"
keywords = ["karaoke", "youtube", "web", "audio", "video", "player", "qt"]
[tool.poetry.scripts] [tool.poetry.scripts]
syng-client = "syng.client:main" syng = "syng.main:main"
syng-server = "syng.server:main"
syng-gui = "syng.gui:main"
# syng-shell = "syng.webclientmockup:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.9"
python-socketio = "^5.10.0" python-socketio = "^5.10.0"
aiohttp = "^3.9.1" aiohttp = "^3.9.1"
pytube = { version = "*", optional = true } yarl = "<1.14.0"
platformdirs = "^4.0.0"
yt-dlp = { version = ">=2024.11.18", extras = ["default"] }
minio = { version = "^7.2.0", optional = true } minio = { version = "^7.2.0", optional = true }
mutagen = { version = "^1.47.0", optional = true }
# aiocmd = "^0.1.5"
pillow = { version = "^10.1.0", optional = true} pillow = { version = "^10.1.0", optional = true}
yt-dlp = { version = "*", optional = true}
customtkinter = { version = "^5.2.1", optional = true}
qrcode = { version = "^7.4.2", optional = true } qrcode = { version = "^7.4.2", optional = true }
pymediainfo = { version = "^6.1.0", optional = true } pymediainfo = { version = "^6.1.0", optional = true }
pyyaml = { version = "^6.0.1", optional = true } pyyaml = { version = "^6.0.1", optional = true }
# async-tkinter-loop = "^0.9.2" alt-profanity-check = {version = "^1.4.1", optional = true}
tkcalendar = { version = "^1.6.1", optional = true } pyqt6 = {version="^6.7.1", optional = true}
tktimepicker = { version = "^2.0.2", optional = true } mpv = {version = "^1.0.7", optional = true}
platformdirs = { version = "^4.0.0", optional = true } qasync = {version = "^0.27.1", optional = true}
packaging = {version = "^23.2", optional = true}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
types-pyyaml = "^6.0.12.12" types-pyyaml = "^6.0.12.12"
types-pillow = "^10.1.0.2" types-pillow = "^10.1.0.2"
mypy = "^1.10.0"
pylint = "^3.2.7"
requirements-parser = "^0.11.0"
[tool.poetry.extras] [tool.poetry.extras]
client = ["minio", "mutagen", "pillow", "yt-dlp", client = ["minio", "pillow", "qrcode", "pymediainfo", "pyyaml", "pyqt6", "mpv", "qasync"]
"customtkinter", "qrcode", "pymediainfo", "pyyaml",
"tkcalendar", "tktimepicker", "platformdirs", "packaging"]
server = ["pytube"]
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
@ -57,9 +70,13 @@ disable = '''too-many-lines,
too-many-ancestors too-many-ancestors
''' '''
[tool.mypy]
mypy_path = "typings"
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = [ module = [
"yt_dlp", "yt_dlp",
"yt_dlp.utils",
"pymediainfo", "pymediainfo",
"minio", "minio",
"qrcode", "qrcode",
@ -74,3 +91,6 @@ ignore_missing_imports = true
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
[tool.black]
line-length = 100

1061
requirements-client.txt Normal file

File diff suppressed because it is too large Load diff

916
requirements-server.txt Normal file
View file

@ -0,0 +1,916 @@
aiohappyeyeballs==2.4.4 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745 \
--hash=sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8
aiohttp==3.10.11 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d \
--hash=sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc \
--hash=sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261 \
--hash=sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b \
--hash=sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625 \
--hash=sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39 \
--hash=sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f \
--hash=sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636 \
--hash=sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac \
--hash=sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217 \
--hash=sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9 \
--hash=sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f \
--hash=sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006 \
--hash=sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e \
--hash=sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9 \
--hash=sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a \
--hash=sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695 \
--hash=sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730 \
--hash=sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e \
--hash=sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382 \
--hash=sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc \
--hash=sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00 \
--hash=sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8 \
--hash=sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658 \
--hash=sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339 \
--hash=sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a \
--hash=sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a \
--hash=sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e \
--hash=sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d \
--hash=sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e \
--hash=sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca \
--hash=sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038 \
--hash=sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461 \
--hash=sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829 \
--hash=sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31 \
--hash=sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4 \
--hash=sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127 \
--hash=sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3 \
--hash=sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97 \
--hash=sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710 \
--hash=sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2 \
--hash=sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674 \
--hash=sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb \
--hash=sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03 \
--hash=sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07 \
--hash=sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9 \
--hash=sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6 \
--hash=sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f \
--hash=sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298 \
--hash=sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b \
--hash=sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01 \
--hash=sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9 \
--hash=sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7 \
--hash=sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413 \
--hash=sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec \
--hash=sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7 \
--hash=sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4 \
--hash=sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb \
--hash=sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519 \
--hash=sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d \
--hash=sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6 \
--hash=sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27 \
--hash=sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1 \
--hash=sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087 \
--hash=sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067 \
--hash=sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f \
--hash=sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95 \
--hash=sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa \
--hash=sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa \
--hash=sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16 \
--hash=sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106 \
--hash=sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92 \
--hash=sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777 \
--hash=sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa \
--hash=sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e \
--hash=sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7 \
--hash=sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115 \
--hash=sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120 \
--hash=sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d \
--hash=sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b \
--hash=sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a \
--hash=sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725 \
--hash=sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a \
--hash=sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d \
--hash=sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288 \
--hash=sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8 \
--hash=sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385 \
--hash=sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24 \
--hash=sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71 \
--hash=sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138 \
--hash=sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177
aiosignal==1.3.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5 \
--hash=sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54
alt-profanity-check==1.6.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:35850f409abaac08db7db52bb3408b9a076b5e96940e12e2196e91f545b13d9b \
--hash=sha256:3da55fac9d674442c8d1a5aece1d96e77290c71da8fe54f99ff34fe490181cca
async-timeout==5.0.1 ; python_version >= "3.9" and python_version < "3.11" \
--hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
--hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
attrs==25.1.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e \
--hash=sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a
bidict==0.23.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71 \
--hash=sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5
brotli==1.1.0 ; implementation_name == "cpython" and python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
brotlicffi==1.1.0.0 ; implementation_name != "cpython" and python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b \
--hash=sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171 \
--hash=sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb \
--hash=sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979 \
--hash=sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33 \
--hash=sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca \
--hash=sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f \
--hash=sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6 \
--hash=sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca \
--hash=sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112 \
--hash=sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391 \
--hash=sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8 \
--hash=sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0 \
--hash=sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35 \
--hash=sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820 \
--hash=sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838 \
--hash=sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613 \
--hash=sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5 \
--hash=sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851 \
--hash=sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814 \
--hash=sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc \
--hash=sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13 \
--hash=sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990 \
--hash=sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6 \
--hash=sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d \
--hash=sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808 \
--hash=sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14
certifi==2025.1.31 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \
--hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
cffi==1.17.1 ; implementation_name != "cpython" and python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
charset-normalizer==3.4.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \
--hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \
--hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \
--hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \
--hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \
--hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \
--hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \
--hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \
--hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \
--hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \
--hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \
--hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \
--hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \
--hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \
--hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \
--hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \
--hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \
--hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \
--hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \
--hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \
--hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \
--hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \
--hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \
--hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \
--hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \
--hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \
--hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \
--hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \
--hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \
--hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \
--hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \
--hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \
--hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \
--hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \
--hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \
--hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \
--hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \
--hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \
--hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \
--hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \
--hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \
--hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \
--hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \
--hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \
--hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \
--hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \
--hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \
--hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \
--hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \
--hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \
--hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \
--hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \
--hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \
--hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \
--hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \
--hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \
--hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \
--hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \
--hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \
--hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \
--hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \
--hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \
--hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \
--hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \
--hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \
--hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \
--hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \
--hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \
--hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \
--hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \
--hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \
--hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \
--hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \
--hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \
--hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \
--hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \
--hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \
--hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \
--hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \
--hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \
--hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \
--hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \
--hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \
--hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \
--hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \
--hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \
--hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \
--hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \
--hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \
--hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \
--hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \
--hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616
frozenlist==1.5.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e \
--hash=sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf \
--hash=sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6 \
--hash=sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a \
--hash=sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d \
--hash=sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f \
--hash=sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28 \
--hash=sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b \
--hash=sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9 \
--hash=sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2 \
--hash=sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec \
--hash=sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2 \
--hash=sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c \
--hash=sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336 \
--hash=sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4 \
--hash=sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d \
--hash=sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b \
--hash=sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c \
--hash=sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10 \
--hash=sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08 \
--hash=sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942 \
--hash=sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8 \
--hash=sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f \
--hash=sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10 \
--hash=sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5 \
--hash=sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6 \
--hash=sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21 \
--hash=sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c \
--hash=sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d \
--hash=sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923 \
--hash=sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608 \
--hash=sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de \
--hash=sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17 \
--hash=sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0 \
--hash=sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f \
--hash=sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641 \
--hash=sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c \
--hash=sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a \
--hash=sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0 \
--hash=sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9 \
--hash=sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab \
--hash=sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f \
--hash=sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3 \
--hash=sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a \
--hash=sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784 \
--hash=sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604 \
--hash=sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d \
--hash=sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5 \
--hash=sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03 \
--hash=sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e \
--hash=sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953 \
--hash=sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee \
--hash=sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d \
--hash=sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817 \
--hash=sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3 \
--hash=sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039 \
--hash=sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f \
--hash=sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9 \
--hash=sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf \
--hash=sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76 \
--hash=sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba \
--hash=sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171 \
--hash=sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb \
--hash=sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439 \
--hash=sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631 \
--hash=sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972 \
--hash=sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d \
--hash=sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869 \
--hash=sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9 \
--hash=sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411 \
--hash=sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723 \
--hash=sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2 \
--hash=sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b \
--hash=sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99 \
--hash=sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e \
--hash=sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840 \
--hash=sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3 \
--hash=sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb \
--hash=sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3 \
--hash=sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0 \
--hash=sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca \
--hash=sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45 \
--hash=sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e \
--hash=sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f \
--hash=sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5 \
--hash=sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307 \
--hash=sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e \
--hash=sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2 \
--hash=sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778 \
--hash=sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a \
--hash=sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30 \
--hash=sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a
h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
idna==3.10 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
joblib==1.4.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6 \
--hash=sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e
multidict==6.1.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f \
--hash=sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056 \
--hash=sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761 \
--hash=sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3 \
--hash=sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b \
--hash=sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6 \
--hash=sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748 \
--hash=sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966 \
--hash=sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f \
--hash=sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1 \
--hash=sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6 \
--hash=sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada \
--hash=sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305 \
--hash=sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2 \
--hash=sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d \
--hash=sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a \
--hash=sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef \
--hash=sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c \
--hash=sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb \
--hash=sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60 \
--hash=sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6 \
--hash=sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4 \
--hash=sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478 \
--hash=sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81 \
--hash=sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7 \
--hash=sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56 \
--hash=sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3 \
--hash=sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6 \
--hash=sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30 \
--hash=sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb \
--hash=sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506 \
--hash=sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0 \
--hash=sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925 \
--hash=sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c \
--hash=sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6 \
--hash=sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e \
--hash=sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95 \
--hash=sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2 \
--hash=sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133 \
--hash=sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2 \
--hash=sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa \
--hash=sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3 \
--hash=sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3 \
--hash=sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436 \
--hash=sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657 \
--hash=sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581 \
--hash=sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492 \
--hash=sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43 \
--hash=sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2 \
--hash=sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2 \
--hash=sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926 \
--hash=sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057 \
--hash=sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc \
--hash=sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80 \
--hash=sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255 \
--hash=sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1 \
--hash=sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972 \
--hash=sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53 \
--hash=sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1 \
--hash=sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423 \
--hash=sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a \
--hash=sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160 \
--hash=sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c \
--hash=sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd \
--hash=sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa \
--hash=sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5 \
--hash=sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b \
--hash=sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa \
--hash=sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef \
--hash=sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44 \
--hash=sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4 \
--hash=sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156 \
--hash=sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753 \
--hash=sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28 \
--hash=sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d \
--hash=sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a \
--hash=sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304 \
--hash=sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008 \
--hash=sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429 \
--hash=sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72 \
--hash=sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399 \
--hash=sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3 \
--hash=sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392 \
--hash=sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167 \
--hash=sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c \
--hash=sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774 \
--hash=sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351 \
--hash=sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76 \
--hash=sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875 \
--hash=sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd \
--hash=sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28 \
--hash=sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db
mutagen==1.47.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
--hash=sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
numpy==2.0.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a \
--hash=sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195 \
--hash=sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951 \
--hash=sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1 \
--hash=sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c \
--hash=sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc \
--hash=sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b \
--hash=sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd \
--hash=sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4 \
--hash=sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd \
--hash=sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318 \
--hash=sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448 \
--hash=sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece \
--hash=sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d \
--hash=sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5 \
--hash=sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8 \
--hash=sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57 \
--hash=sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78 \
--hash=sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66 \
--hash=sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a \
--hash=sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e \
--hash=sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c \
--hash=sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa \
--hash=sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d \
--hash=sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c \
--hash=sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729 \
--hash=sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97 \
--hash=sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c \
--hash=sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9 \
--hash=sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669 \
--hash=sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4 \
--hash=sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73 \
--hash=sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385 \
--hash=sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8 \
--hash=sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c \
--hash=sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b \
--hash=sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692 \
--hash=sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15 \
--hash=sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131 \
--hash=sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a \
--hash=sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326 \
--hash=sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b \
--hash=sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded \
--hash=sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04 \
--hash=sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd
platformdirs==4.3.6 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
pycparser==2.22 ; implementation_name != "cpython" and python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
pycryptodomex==3.21.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3 \
--hash=sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516 \
--hash=sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f \
--hash=sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c \
--hash=sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e \
--hash=sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e \
--hash=sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c \
--hash=sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31 \
--hash=sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b \
--hash=sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832 \
--hash=sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e \
--hash=sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b \
--hash=sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37 \
--hash=sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65 \
--hash=sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a \
--hash=sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3 \
--hash=sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b \
--hash=sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9 \
--hash=sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971 \
--hash=sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2 \
--hash=sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42 \
--hash=sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd \
--hash=sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e \
--hash=sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0 \
--hash=sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c \
--hash=sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a \
--hash=sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce \
--hash=sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6 \
--hash=sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822 \
--hash=sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9 \
--hash=sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00 \
--hash=sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8
python-engineio==4.11.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:145bb0daceb904b4bb2d3eb2d93f7dbb7bb87a6a0c4f20a94cc8654dec977129 \
--hash=sha256:f0971ac4c65accc489154fe12efd88f53ca8caf04754c46a66e85f5102ef22ad
python-socketio==5.12.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0299ff1f470b676c09c1bfab1dead25405077d227b2c13cf217a34dadc68ba9c \
--hash=sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386
requests==2.32.3 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
scikit-learn==1.6.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691 \
--hash=sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36 \
--hash=sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f \
--hash=sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8 \
--hash=sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2 \
--hash=sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86 \
--hash=sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322 \
--hash=sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f \
--hash=sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107 \
--hash=sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1 \
--hash=sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35 \
--hash=sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52 \
--hash=sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33 \
--hash=sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b \
--hash=sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb \
--hash=sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b \
--hash=sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5 \
--hash=sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002 \
--hash=sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b \
--hash=sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236 \
--hash=sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d \
--hash=sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e \
--hash=sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422 \
--hash=sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348 \
--hash=sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e \
--hash=sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2 \
--hash=sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1 \
--hash=sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e \
--hash=sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97 \
--hash=sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415
scipy==1.13.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d \
--hash=sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c \
--hash=sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca \
--hash=sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9 \
--hash=sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54 \
--hash=sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16 \
--hash=sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2 \
--hash=sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5 \
--hash=sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59 \
--hash=sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326 \
--hash=sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b \
--hash=sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1 \
--hash=sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d \
--hash=sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24 \
--hash=sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627 \
--hash=sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c \
--hash=sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa \
--hash=sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949 \
--hash=sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989 \
--hash=sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004 \
--hash=sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f \
--hash=sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884 \
--hash=sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299 \
--hash=sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94 \
--hash=sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f
simple-websocket==1.1.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c \
--hash=sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4
threadpoolctl==3.5.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107 \
--hash=sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "3.11" \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
urllib3==2.3.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
websockets==14.2 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a \
--hash=sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267 \
--hash=sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda \
--hash=sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c \
--hash=sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9 \
--hash=sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397 \
--hash=sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0 \
--hash=sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142 \
--hash=sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910 \
--hash=sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c \
--hash=sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2 \
--hash=sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205 \
--hash=sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473 \
--hash=sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c \
--hash=sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258 \
--hash=sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661 \
--hash=sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d \
--hash=sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166 \
--hash=sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365 \
--hash=sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce \
--hash=sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8 \
--hash=sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad \
--hash=sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7 \
--hash=sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5 \
--hash=sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f \
--hash=sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967 \
--hash=sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a \
--hash=sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4 \
--hash=sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990 \
--hash=sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a \
--hash=sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e \
--hash=sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610 \
--hash=sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d \
--hash=sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d \
--hash=sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b \
--hash=sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe \
--hash=sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc \
--hash=sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b \
--hash=sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f \
--hash=sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0 \
--hash=sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473 \
--hash=sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3 \
--hash=sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42 \
--hash=sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5 \
--hash=sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc \
--hash=sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307 \
--hash=sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574 \
--hash=sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95 \
--hash=sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f \
--hash=sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef \
--hash=sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f \
--hash=sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f \
--hash=sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5 \
--hash=sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c \
--hash=sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f \
--hash=sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2 \
--hash=sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29 \
--hash=sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7 \
--hash=sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3 \
--hash=sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980 \
--hash=sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885 \
--hash=sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe \
--hash=sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20 \
--hash=sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12 \
--hash=sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56 \
--hash=sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3 \
--hash=sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270 \
--hash=sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03 \
--hash=sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2
wsproto==1.2.0 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \
--hash=sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736
yarl==1.13.1 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b \
--hash=sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424 \
--hash=sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644 \
--hash=sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465 \
--hash=sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88 \
--hash=sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8 \
--hash=sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da \
--hash=sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154 \
--hash=sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51 \
--hash=sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f \
--hash=sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc \
--hash=sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d \
--hash=sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8 \
--hash=sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4 \
--hash=sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c \
--hash=sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc \
--hash=sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2 \
--hash=sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606 \
--hash=sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6 \
--hash=sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c \
--hash=sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734 \
--hash=sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae \
--hash=sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220 \
--hash=sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e \
--hash=sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d \
--hash=sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c \
--hash=sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177 \
--hash=sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da \
--hash=sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23 \
--hash=sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485 \
--hash=sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85 \
--hash=sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05 \
--hash=sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09 \
--hash=sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99 \
--hash=sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9 \
--hash=sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e \
--hash=sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5 \
--hash=sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71 \
--hash=sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0 \
--hash=sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8 \
--hash=sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10 \
--hash=sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246 \
--hash=sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049 \
--hash=sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d \
--hash=sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2 \
--hash=sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad \
--hash=sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323 \
--hash=sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7 \
--hash=sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2 \
--hash=sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3 \
--hash=sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a \
--hash=sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851 \
--hash=sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206 \
--hash=sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b \
--hash=sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550 \
--hash=sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f \
--hash=sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1 \
--hash=sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b \
--hash=sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe \
--hash=sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74 \
--hash=sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5 \
--hash=sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495 \
--hash=sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c \
--hash=sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813 \
--hash=sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a \
--hash=sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2 \
--hash=sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d \
--hash=sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57 \
--hash=sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc \
--hash=sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320 \
--hash=sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43 \
--hash=sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e \
--hash=sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac \
--hash=sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26 \
--hash=sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c \
--hash=sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2 \
--hash=sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799 \
--hash=sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d \
--hash=sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735 \
--hash=sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419 \
--hash=sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e \
--hash=sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4 \
--hash=sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0 \
--hash=sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c \
--hash=sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3 \
--hash=sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8 \
--hash=sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9 \
--hash=sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224 \
--hash=sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38 \
--hash=sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf \
--hash=sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7 \
--hash=sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093
yt-dlp[default]==2025.1.26 ; python_version >= "3.9" and python_version < "4.0" \
--hash=sha256:1c9738266921ad43c568ad01ac3362fb7c7af549276fbec92bd72f140da16240 \
--hash=sha256:3e76bd896b9f96601021ca192ca0fbdd195e3c3dcc28302a3a34c9bc4979da7b

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
FROM python:3.12-bullseye
RUN useradd -m -d /app syng
USER syng
ENV PATH="/app/.local/bin:${PATH}"
WORKDIR /app/
RUN pip install --user "syng[server]@git+https://github.com/christofsteel/syng.git"
RUN touch /app/keys.txt
EXPOSE 8080
ENTRYPOINT ["syng", "server", "-k", "/app/keys.txt"]

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
./flatpak-pip-generator --build-only --yaml poetry-core
./flatpak-pip-generator --build-only --yaml expandvars
./flatpak-pip-generator --yaml cffi
awk -v package="pyqt6" '
BEGIN { inside_block = 0 }
# Handle continuation lines
/\\$/ {
if (inside_block == 0 && $0 ~ package) { inside_block = 1 }
if (inside_block == 1) { next }
}
{
# End of a multi-line block
if (inside_block == 1 && !/\\$/) { inside_block = 0; next }
if (inside_block == 0 && $0 ~ package) { next }
print
}
' "../../requirements-client.txt" > "requirements-client.txt"
./flatpak-pip-generator --requirements-file requirements-client.txt --ignore-pkg cffi==1.17.1 --yaml

View file

@ -0,0 +1,546 @@
#!/usr/bin/env python3
__license__ = "MIT"
import argparse
import json
import hashlib
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.request
from collections import OrderedDict
from typing import Dict
try:
import requirements
except ImportError:
exit('Requirements modules is not installed. Run "pip install requirements-parser"')
parser = argparse.ArgumentParser()
parser.add_argument("packages", nargs="*")
parser.add_argument("--python2", action="store_true", help="Look for a Python 2 package")
parser.add_argument(
"--cleanup", choices=["scripts", "all"], help="Select what to clean up after build"
)
parser.add_argument("--requirements-file", "-r", help="Specify requirements.txt file")
parser.add_argument(
"--build-only",
action="store_const",
dest="cleanup",
const="all",
help="Clean up all files after build",
)
parser.add_argument(
"--build-isolation",
action="store_true",
default=False,
help=(
"Do not disable build isolation. "
"Mostly useful on pip that does't "
"support the feature."
),
)
parser.add_argument(
"--ignore-installed",
type=lambda s: s.split(","),
default="",
help="Comma-separated list of package names for which pip "
"should ignore already installed packages. Useful when "
"the package is installed in the SDK but not in the "
"runtime.",
)
parser.add_argument(
"--checker-data",
action="store_true",
help='Include x-checker-data in output for the "Flatpak External Data Checker"',
)
parser.add_argument("--output", "-o", help="Specify output file name")
parser.add_argument(
"--runtime",
help="Specify a flatpak to run pip inside of a sandbox, ensures python version compatibility",
)
parser.add_argument("--yaml", action="store_true", help="Use YAML as output format instead of JSON")
parser.add_argument(
"--ignore-errors", action="store_true", help="Ignore errors when downloading packages"
)
parser.add_argument(
"--ignore-pkg",
nargs="*",
help="Ignore a package when generating the manifest. Can only be used with a requirements file",
)
opts = parser.parse_args()
if opts.yaml:
try:
import yaml
except ImportError:
exit('PyYAML modules is not installed. Run "pip install PyYAML"')
def get_pypi_url(name: str, filename: str) -> str:
url = "https://pypi.org/pypi/{}/json".format(name)
print("Extracting download url for", name)
with urllib.request.urlopen(url) as response:
body = json.loads(response.read().decode("utf-8"))
for release in body["releases"].values():
for source in release:
if source["filename"] == filename:
return source["url"]
raise Exception("Failed to extract url from {}".format(url))
def get_tar_package_url_pypi(name: str, version: str) -> str:
url = "https://pypi.org/pypi/{}/{}/json".format(name, version)
with urllib.request.urlopen(url) as response:
body = json.loads(response.read().decode("utf-8"))
for ext in ["bz2", "gz", "xz", "zip"]:
for source in body["urls"]:
if source["url"].endswith(ext):
return source["url"]
err = "Failed to get {}-{} source from {}".format(name, version, url)
raise Exception(err)
def get_package_name(filename: str) -> str:
if filename.endswith(("bz2", "gz", "xz", "zip")):
segments = filename.split("-")
if len(segments) == 2:
return segments[0]
return "-".join(segments[: len(segments) - 1])
elif filename.endswith("whl"):
segments = filename.split("-")
if len(segments) == 5:
return segments[0]
candidate = segments[: len(segments) - 4]
# Some packages list the version number twice
# e.g. PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl
if candidate[-1] == segments[len(segments) - 4]:
return "-".join(candidate[:-1])
return "-".join(candidate)
else:
raise Exception(
"Downloaded filename: {} does not end with bz2, gz, xz, zip, or whl".format(filename)
)
def get_file_version(filename: str) -> str:
name = get_package_name(filename)
segments = filename.split(name + "-")
version = segments[1].split("-")[0]
for ext in ["tar.gz", "whl", "tar.xz", "tar.gz", "tar.bz2", "zip"]:
version = version.replace("." + ext, "")
return version
def get_file_hash(filename: str) -> str:
sha = hashlib.sha256()
print("Generating hash for", filename.split("/")[-1])
with open(filename, "rb") as f:
while True:
data = f.read(1024 * 1024 * 32)
if not data:
break
sha.update(data)
return sha.hexdigest()
def download_tar_pypi(url: str, tempdir: str) -> None:
with urllib.request.urlopen(url) as response:
file_path = os.path.join(tempdir, url.split("/")[-1])
with open(file_path, "x+b") as tar_file:
shutil.copyfileobj(response, tar_file)
def parse_continuation_lines(fin):
for line in fin:
line = line.rstrip("\n")
while line.endswith("\\"):
try:
line = line[:-1] + next(fin).rstrip("\n")
except StopIteration:
exit('Requirements have a wrong number of line continuation characters "\\"')
yield line
def fprint(string: str) -> None:
separator = "=" * 72 # Same as `flatpak-builder`
print(separator)
print(string)
print(separator)
packages = []
if opts.requirements_file:
requirements_file_input = os.path.expanduser(opts.requirements_file)
try:
with open(requirements_file_input, "r") as req_file:
reqs = parse_continuation_lines(req_file)
reqs_as_str = "\n".join([r.split("--hash")[0] for r in reqs])
reqs_list_raw = reqs_as_str.splitlines()
py_version_regex = re.compile(
r";.*python_version .+$"
) # Remove when pip-generator can handle python_version
reqs_list = [py_version_regex.sub("", p) for p in reqs_list_raw]
if opts.ignore_pkg:
reqs_new = "\n".join(i for i in reqs_list if i not in opts.ignore_pkg)
else:
reqs_new = reqs_as_str
packages = list(requirements.parse(reqs_new))
with tempfile.NamedTemporaryFile("w", delete=False, prefix="requirements.") as req_file:
req_file.write(reqs_new)
requirements_file_output = req_file.name
except FileNotFoundError as err:
print(err)
sys.exit(1)
elif opts.packages:
packages = list(requirements.parse("\n".join(opts.packages)))
with tempfile.NamedTemporaryFile("w", delete=False, prefix="requirements.") as req_file:
req_file.write("\n".join(opts.packages))
requirements_file_output = req_file.name
else:
if not len(sys.argv) > 1:
exit("Please specifiy either packages or requirements file argument")
else:
exit("This option can only be used with requirements file")
qt = []
for i in packages:
if i["name"].lower().startswith("pyqt"):
print("PyQt packages are not supported by flapak-pip-generator")
print("However, there is a BaseApp for PyQt available, that you should use")
print(
"Visit https://github.com/flathub/com.riverbankcomputing.PyQt.BaseApp for more information"
)
# sys.exit(0)
print("Ignoring", i["name"])
qt.append(i)
packages = [i for i in packages if i not in qt]
with open(requirements_file_output, "r") as req_file:
use_hash = "--hash=" in req_file.read()
python_version = "2" if opts.python2 else "3"
if opts.python2:
pip_executable = "pip2"
else:
pip_executable = "pip3"
if opts.runtime:
flatpak_cmd = [
"flatpak",
"--devel",
"--share=network",
"--filesystem=/tmp",
"--command={}".format(pip_executable),
"run",
opts.runtime,
]
if opts.requirements_file:
if os.path.exists(requirements_file_output):
prefix = os.path.realpath(requirements_file_output)
flag = "--filesystem={}".format(prefix)
flatpak_cmd.insert(1, flag)
else:
flatpak_cmd = [pip_executable]
output_path = ""
if opts.output:
output_path = os.path.dirname(opts.output)
output_package = os.path.basename(opts.output)
elif opts.requirements_file:
output_package = "python{}-{}".format(
python_version,
os.path.basename(opts.requirements_file).replace(".txt", ""),
)
elif len(packages) == 1:
output_package = "python{}-{}".format(
python_version,
packages[0].name,
)
else:
output_package = "python{}-modules".format(python_version)
if opts.yaml:
output_filename = os.path.join(output_path, output_package) + ".yaml"
else:
output_filename = os.path.join(output_path, output_package) + ".json"
modules = []
vcs_modules = []
sources = {}
tempdir_prefix = "pip-generator-{}".format(output_package)
with tempfile.TemporaryDirectory(prefix=tempdir_prefix) as tempdir:
pip_download = flatpak_cmd + [
"download",
"--exists-action=i",
"--dest",
tempdir,
"-r",
requirements_file_output,
]
if use_hash:
pip_download.append("--require-hashes")
fprint("Downloading sources")
cmd = " ".join(pip_download)
print('Running: "{}"'.format(cmd))
try:
subprocess.run(pip_download, check=True)
os.remove(requirements_file_output)
except subprocess.CalledProcessError:
os.remove(requirements_file_output)
print("Failed to download")
print("Please fix the module manually in the generated file")
if not opts.ignore_errors:
print("Ignore the error by passing --ignore-errors")
raise
try:
os.remove(requirements_file_output)
except FileNotFoundError:
pass
fprint("Downloading arch independent packages")
for filename in os.listdir(tempdir):
if not filename.endswith(("bz2", "any.whl", "gz", "xz", "zip")):
version = get_file_version(filename)
name = get_package_name(filename)
url = get_tar_package_url_pypi(name, version)
print("Deleting", filename)
try:
os.remove(os.path.join(tempdir, filename))
except FileNotFoundError:
pass
print("Downloading {}".format(url))
download_tar_pypi(url, tempdir)
files = {get_package_name(f): [] for f in os.listdir(tempdir)}
for filename in os.listdir(tempdir):
name = get_package_name(filename)
files[name].append(filename)
# Delete redundant sources, for vcs sources
for name in files:
if len(files[name]) > 1:
zip_source = False
for f in files[name]:
if f.endswith(".zip"):
zip_source = True
if zip_source:
for f in files[name]:
if not f.endswith(".zip"):
try:
os.remove(os.path.join(tempdir, f))
except FileNotFoundError:
pass
vcs_packages = {
x.name: {"vcs": x.vcs, "revision": x.revision, "uri": x.uri} for x in packages if x.vcs
}
fprint("Obtaining hashes and urls")
for filename in os.listdir(tempdir):
name = get_package_name(filename)
sha256 = get_file_hash(os.path.join(tempdir, filename))
is_pypi = False
if name in vcs_packages:
uri = vcs_packages[name]["uri"]
revision = vcs_packages[name]["revision"]
vcs = vcs_packages[name]["vcs"]
url = "https://" + uri.split("://", 1)[1]
s = "commit"
if vcs == "svn":
s = "revision"
source = OrderedDict(
[
("type", vcs),
("url", url),
(s, revision),
]
)
is_vcs = True
else:
name = name.casefold()
is_pypi = True
url = get_pypi_url(name, filename)
source = OrderedDict([("type", "file"), ("url", url), ("sha256", sha256)])
if opts.checker_data:
source["x-checker-data"] = {"type": "pypi", "name": name}
if url.endswith(".whl"):
source["x-checker-data"]["packagetype"] = "bdist_wheel"
is_vcs = False
sources[name] = {"source": source, "vcs": is_vcs, "pypi": is_pypi}
# Python3 packages that come as part of org.freedesktop.Sdk.
system_packages = [
"cython",
"easy_install",
"mako",
"markdown",
"meson",
"pip",
"pygments",
"setuptools",
"six",
"wheel",
]
fprint("Generating dependencies")
for package in packages:
if package.name is None:
print(
"Warning: skipping invalid requirement specification {} because it is missing a name".format(
package.line
),
file=sys.stderr,
)
print("Append #egg=<pkgname> to the end of the requirement line to fix", file=sys.stderr)
continue
elif package.name.casefold() in system_packages:
print(f"{package.name} is in system_packages. Skipping.")
continue
if len(package.extras) > 0:
extras = "[" + ",".join(extra for extra in package.extras) + "]"
else:
extras = ""
version_list = [x[0] + x[1] for x in package.specs]
version = ",".join(version_list)
if package.vcs:
revision = ""
if package.revision:
revision = "@" + package.revision
pkg = package.uri + revision + "#egg=" + package.name
else:
pkg = package.name + extras + version
dependencies = []
# Downloads the package again to list dependencies
tempdir_prefix = "pip-generator-{}".format(package.name)
with tempfile.TemporaryDirectory(
prefix="{}-{}".format(tempdir_prefix, package.name)
) as tempdir:
pip_download = flatpak_cmd + [
"download",
"--exists-action=i",
"--dest",
tempdir,
]
try:
print("Generating dependencies for {}".format(package.name))
subprocess.run(pip_download + [pkg], check=True, stdout=subprocess.DEVNULL)
for filename in sorted(os.listdir(tempdir)):
dep_name = get_package_name(filename)
if dep_name.casefold() in system_packages:
continue
dependencies.append(dep_name)
except subprocess.CalledProcessError:
print("Failed to download {}".format(package.name))
is_vcs = True if package.vcs else False
package_sources = []
for dependency in dependencies:
casefolded = dependency.casefold()
if casefolded in sources and sources[casefolded].get("pypi") is True:
source = sources[casefolded]
elif dependency in sources and sources[dependency].get("pypi") is False:
source = sources[dependency]
elif (
casefolded.replace("_", "-") in sources
and sources[casefolded.replace("_", "-")].get("pypi") is True
):
source = sources[casefolded.replace("_", "-")]
elif (
dependency.replace("_", "-") in sources
and sources[dependency.replace("_", "-")].get("pypi") is False
):
source = sources[dependency.replace("_", "-")]
else:
continue
if not (not source["vcs"] or is_vcs):
continue
package_sources.append(source["source"])
if package.vcs:
name_for_pip = "."
else:
name_for_pip = pkg
module_name = "python{}-{}".format(python_version, package.name)
pip_command = [
pip_executable,
"install",
"--verbose",
"--exists-action=i",
"--no-index",
'--find-links="file://${PWD}"',
"--prefix=${FLATPAK_DEST}",
'"{}"'.format(name_for_pip),
]
if package.name in opts.ignore_installed:
pip_command.append("--ignore-installed")
if not opts.build_isolation:
pip_command.append("--no-build-isolation")
module = OrderedDict(
[
("name", module_name),
("buildsystem", "simple"),
("build-commands", [" ".join(pip_command)]),
("sources", package_sources),
]
)
if opts.cleanup == "all":
module["cleanup"] = ["*"]
elif opts.cleanup == "scripts":
module["cleanup"] = ["/bin", "/share/man/man1"]
if package.vcs:
vcs_modules.append(module)
else:
modules.append(module)
modules = vcs_modules + modules
if len(modules) == 1:
pypi_module = modules[0]
else:
pypi_module = {
"name": output_package,
"buildsystem": "simple",
"build-commands": [],
"modules": modules,
}
print()
with open(output_filename, "w") as output:
if opts.yaml:
class OrderedDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False):
return super(OrderedDumper, self).increase_indent(flow, False)
def dict_representer(dumper, data):
return dumper.represent_dict(data.items())
OrderedDumper.add_representer(OrderedDict, dict_representer)
output.write("# Generated with flatpak-pip-generator " + " ".join(sys.argv[1:]) + "\n")
yaml.dump(pypi_module, output, Dumper=OrderedDumper)
else:
output.write(json.dumps(pypi_module, indent=4))
print("Output saved to {}".format(output_filename))

View file

@ -0,0 +1,13 @@
# Generated with flatpak-pip-generator --yaml cffi
name: python3-cffi
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "cffi" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz
sha256: 1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824
- type: file
url: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl
sha256: c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc

View file

@ -0,0 +1,12 @@
# Generated with flatpak-pip-generator --build-only --yaml expandvars
name: python3-expandvars
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "expandvars" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/df/b3/072c28eace372ba7630ea187b7efd7f09cc8bcebf847a96b5e03e9cc0828/expandvars-0.12.0-py3-none-any.whl
sha256: 7432c1c2ae50c671a8146583177d60020dd210ada7d940e52af91f1f84f753b2
cleanup:
- '*'

View file

@ -0,0 +1,12 @@
# Generated with flatpak-pip-generator --build-only --yaml poetry-core
name: python3-poetry-core
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "poetry-core" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/f7/b4/ae500aaba6e003ff80889e3dee449b154d2dd70d520dc0402f23535a5995/poetry_core-1.9.1-py3-none-any.whl
sha256: 6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0
cleanup:
- '*'

View file

@ -0,0 +1,453 @@
# Generated with flatpak-pip-generator --requirements-file requirements-client.txt --ignore-pkg cffi==1.17.1 --yaml
build-commands: []
buildsystem: simple
modules:
- name: python3-aiohappyeyeballs
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "aiohappyeyeballs==2.4.3" --no-build-isolation
sources:
- &id001
type: file
url: https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl
sha256: 8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572
- name: python3-aiohttp
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "aiohttp==3.10.11" --no-build-isolation
sources:
- *id001
- type: file
url: https://files.pythonhosted.org/packages/25/a8/8e2ba36c6e3278d62e0c88aa42bb92ddbef092ac363b390dab4421da5cf5/aiohttp-3.10.11.tar.gz
sha256: 9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7
- &id002
type: file
url: https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl
sha256: f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17
- &id007
type: file
url: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl
sha256: 81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
- &id003
type: file
url: https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz
sha256: 81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817
- &id008
type: file
url: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl
sha256: 946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
- &id011
type: file
url: https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz
sha256: 22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a
- &id022
type: file
url: https://files.pythonhosted.org/packages/e0/11/2b8334f4192646677a2e7da435670d043f536088af943ec242f31453e5ba/yarl-1.13.1.tar.gz
sha256: ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0
- name: python3-aiosignal
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "aiosignal==1.3.1" --no-build-isolation
sources:
- *id002
- *id003
- name: python3-argon2-cffi-bindings
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "argon2-cffi-bindings==21.2.0" --no-build-isolation
sources:
- &id004
type: file
url: https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz
sha256: bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3
- &id005
type: file
url: https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz
sha256: 1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824
- &id006
type: file
url: https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl
sha256: c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
- name: python3-argon2-cffi
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "argon2-cffi==23.1.0" --no-build-isolation
sources:
- &id009
type: file
url: https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl
sha256: c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea
- *id004
- *id005
- *id006
- name: python3-async-timeout
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "async-timeout==5.0.1" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl
sha256: 39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c
- name: python3-attrs
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "attrs==24.2.0" --no-build-isolation
sources:
- *id007
- name: python3-bidict
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "bidict==0.23.1" --no-build-isolation
sources:
- &id014
type: file
url: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl
sha256: 5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5
- name: python3-brotli
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "brotli==1.1.0" --no-build-isolation
sources:
- &id023
type: file
url: https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz
sha256: 81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724
- name: python3-brotlicffi
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "brotlicffi==1.1.0.0" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz
sha256: b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13
- *id005
- *id006
- name: python3-certifi
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "certifi==2024.8.30" --no-build-isolation
sources:
- &id010
type: file
url: https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl
sha256: 922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8
- name: python3-cffi
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "cffi==1.17.1" --no-build-isolation
sources:
- *id005
- *id006
- name: python3-charset-normalizer
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "charset-normalizer==3.4.0" --no-build-isolation
sources:
- &id020
type: file
url: https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz
sha256: 223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e
- name: python3-colorama
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "colorama==0.4.6" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl
sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
- name: python3-frozenlist
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "frozenlist==1.5.0" --no-build-isolation
sources:
- *id003
- name: python3-h11
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "h11==0.14.0" --no-build-isolation
sources:
- &id013
type: file
url: https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl
sha256: e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
- name: python3-idna
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "idna==3.10" --no-build-isolation
sources:
- *id008
- name: python3-minio
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "minio==7.2.10" --no-build-isolation
sources:
- *id009
- *id004
- *id010
- *id005
- type: file
url: https://files.pythonhosted.org/packages/20/6f/1b1f5025bf43c2a4ca8112332db586c8077048ec8bcea2deb269eac84577/minio-7.2.10-py3-none-any.whl
sha256: 5961c58192b1d70d3a2a362064b8e027b8232688998a6d1251dadbb02ab57a7d
- *id006
- &id012
type: file
url: https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz
sha256: f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297
- &id019
type: file
url: https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl
sha256: 04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d
- &id021
type: file
url: https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl
sha256: ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac
- name: python3-mpv
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "mpv==1.0.7" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/aa/3f/d835556e34804cd0078507ed0f8a550f15d2861b875656193dd3451b720b/mpv-1.0.7-py3-none-any.whl
sha256: 520fb134c18185b69c7fce4aa3514f14371028022d92eb193818e9fefb1e9fe8
- name: python3-multidict
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "multidict==6.1.0" --no-build-isolation
sources:
- *id011
- name: python3-mutagen
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "mutagen==1.47.0" --no-build-isolation
sources:
- &id024
type: file
url: https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl
sha256: edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
- name: python3-pillow
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pillow==10.4.0" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz
sha256: 166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06
- name: python3-platformdirs
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "platformdirs==4.3.6" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl
sha256: 73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
- name: python3-pycparser
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pycparser==2.22" --no-build-isolation
sources:
- *id006
- name: python3-pycryptodome
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pycryptodome==3.21.0" --no-build-isolation
sources:
- *id012
- name: python3-pycryptodomex
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pycryptodomex==3.21.0" --no-build-isolation
sources:
- &id025
type: file
url: https://files.pythonhosted.org/packages/11/dc/e66551683ade663b5f07d7b3bc46434bf703491dbd22ee12d1f979ca828f/pycryptodomex-3.21.0.tar.gz
sha256: 222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c
- name: python3-pymediainfo
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pymediainfo==6.1.0" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/0f/ed/a02b18943f9162644f90354fe6445410e942c857dd21ded758f630ba41c0/pymediainfo-6.1.0.tar.gz
sha256: 186a0b41a94524f0984d085ca6b945c79a254465b7097f2560dc0c04e8d1d8a5
- name: python3-pypng
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pypng==0.20220715.0" --no-build-isolation
sources:
- &id018
type: file
url: https://files.pythonhosted.org/packages/3e/b9/3766cc361d93edb2ce81e2e1f87dd98f314d7d513877a342d31b30741680/pypng-0.20220715.0-py3-none-any.whl
sha256: 4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c
- name: python3-python-engineio
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "python-engineio==4.10.1" --no-build-isolation
sources:
- *id013
- &id015
type: file
url: https://files.pythonhosted.org/packages/fc/74/1cec7f067ade8ed0351aed93ae6cfcd070e803d60fa70ecac6705de62936/python_engineio-4.10.1-py3-none-any.whl
sha256: 445a94004ec8034960ab99e7ce4209ec619c6e6b6a12aedcb05abeab924025c0
- &id016
type: file
url: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl
sha256: 4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c
- &id017
type: file
url: https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl
sha256: b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736
- name: python3-python-socketio
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "python-socketio==5.11.4" --no-build-isolation
sources:
- *id014
- *id013
- *id015
- type: file
url: https://files.pythonhosted.org/packages/7e/9a/52b94c8c9516e07844d3da3d0da3e68649f172aeeace8d7a1becca9e6111/python_socketio-5.11.4-py3-none-any.whl
sha256: 42efaa3e3e0b166fc72a527488a13caaac2cefc76174252486503bd496284945
- *id016
- *id017
- name: python3-pyyaml
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pyyaml==6.0.2" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz
sha256: d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e
- name: python3-qasync
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "qasync==0.27.1" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/51/06/bc628aa2981bcfd452a08ee435b812fd3eee4ada8acb8a76c4a09d1a5a77/qasync-0.27.1-py3-none-any.whl
sha256: 5d57335723bc7d9b328dadd8cb2ed7978640e4bf2da184889ce50ee3ad2602c7
- name: python3-qrcode
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "qrcode==7.4.2" --no-build-isolation
sources:
- *id018
- type: file
url: https://files.pythonhosted.org/packages/24/79/aaf0c1c7214f2632badb2771d770b1500d3d7cbdf2590ae62e721ec50584/qrcode-7.4.2-py3-none-any.whl
sha256: 581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a
- *id019
- name: python3-requests
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "requests==2.32.3" --no-build-isolation
sources:
- *id010
- *id020
- *id008
- &id026
type: file
url: https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl
sha256: 70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
- *id021
- name: python3-simple-websocket
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "simple-websocket==1.1.0" --no-build-isolation
sources:
- *id013
- *id016
- *id017
- name: python3-typing-extensions
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "typing-extensions==4.12.2" --no-build-isolation
sources:
- *id019
- name: python3-urllib3
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "urllib3==2.2.3" --no-build-isolation
sources:
- *id021
- name: python3-websockets
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "websockets==13.1" --no-build-isolation
sources:
- &id027
type: file
url: https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz
sha256: a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878
- name: python3-wsproto
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "wsproto==1.2.0" --no-build-isolation
sources:
- *id013
- *id017
- name: python3-yarl
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "yarl==1.13.1" --no-build-isolation
sources:
- *id008
- *id011
- *id022
- name: python3-yt-dlp
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "yt-dlp[default]==2024.11.18" --no-build-isolation
sources:
- *id023
- *id010
- *id020
- *id008
- *id024
- *id025
- *id026
- *id021
- *id027
- type: file
url: https://files.pythonhosted.org/packages/64/22/1918d2c8c123e9157efd7c2063ea89b4826f904d67b17e77152862ac3347/yt_dlp-2024.11.18-py3-none-any.whl
sha256: b9741695911dc566498b5f115cdd6b1abbc5be61cb01fd98abe649990a41656c
name: python3-requirements-client

View file

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>rocks.syng.Syng</id>
<name>Syng</name>
<summary>Easily host karaoke events</summary>
<developer id="rocks.syng">
<name>Christoph Stahl</name>
</developer>
<metadata_license>CC-BY-SA-4.0</metadata_license>
<project_license>AGPL-3.0-or-later</project_license>
<content_rating type="oars-1.1" />
<url type="homepage">https://syng.rocks</url>
<url type="bugtracker">https://github.com/christofsteel/syng/issues</url>
<url type="vcs-browser">https://github.com/christofsteel/syng</url>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng.png</image>
<caption>Syng configuration window</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_advanced.png</image>
<caption>Syng configuration window (Advanced settings)</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_player_next_up.png</image>
<caption>Next up screen</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_player_song.png</image>
<caption>Player playing a song</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_mobile_search.png</image>
<caption>Web Interface on Mobile</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/christofsteel/syng/94e0d9c0b77579ed256bf74412a20da314dd7166/resources/screenshots/syng_web2.png</image>
<caption>Syng web interface</caption>
</screenshot>
</screenshots>
<releases>
<release version="2.1.0" date="2024-11-21">
<description>
<p>
<em>Changes</em>:
</p>
<ul>
<li>Only opens a single video window.</li>
<li>Added branding and qr code to video window</li>
<li>Better buffering options</li>
</ul>
</description>
</release>
<release version="2.0.7" date="2024-11-18">
<description>
<p>
<em>Bug Fixes</em>:
</p>
<ul>
<li>Fixed local YT search.</li>
<li>Fixeds metadata fetch for directly added YT videos.</li>
</ul>
</description>
</release>
<release version="2.0.6" date="2024-11-18">
<description>
<p>
<em>Changes</em>:
</p>
<ul>
<li>Update to latest version of yt-dlp. Search issues should be resolved.</li>
</ul>
</description>
</release>
<release version="2.0.5" date="2024-11-16">
<description>
<p>
<em>Changes</em>:
</p>
<ul>
<li>Downgraded yt-dlp due to search issues</li>
</ul>
</description>
</release>
<release version="2.0.4" date="2024-11-15">
<description>
<p>
<em>Bug fixes</em>:
</p>
<ul>
<li>Fixed a bug, that could lead to a deadlock in the player</li>
</ul>
</description>
</release>
<release version="2.0.3" date="2024-10-10">
<description>
<p>
<em>Bug fixes</em>:
</p>
<ul>
<li>More informative errors, if GUI could not start</li>
<li>Fixed <code>Cannot open file ''</code> error</li>
</ul>
</description>
</release>
<release version="2.0.2" date="2024-10-06">
<description>
<p>
<em>Changes</em>:
</p>
<ul>
<li>Simplified user interface</li>
<li>Added config option to add parameters to mpv</li>
<li>Index files are now updated in the background after starting</li>
</ul>
</description>
</release>
<release version="2.0.1" date="2024-09-30">
<description>
<p>
<em>Fixes</em>:
</p>
<ul>
<li>Fixes s3 storage not working</li>
</ul>
<p>
<em>Changes</em>:
</p>
<ul>
<li>Notifications are forwarded to the user interface</li>
</ul>
</description>
</release>
<release version="2.0.0" date="2024-09-22">
<description>
<p>
Initial release of version 2.
</p>
</description>
</release>
</releases>
<description>
<p>
Syng is an easy-to-use karaoke software. Just start the client and let your users connect via their web browser.
You can set up your own <em>Syng server</em> or simply use a publicly available one.
</p>
<p>
Songs can be searched and played from <em>YouTube</em>, an <em>S3</em> storage or <em>your local machine</em>.
</p>
<p>
You can play a variety of file formats, such as <code>mp3+cdg</code>, <code>webm</code>, <code>mp4</code>, ...
</p>
</description>
<categories>
<category>X-Karaoke</category>
<category>Video</category>
<category>Audio</category>
<category>AudioVideo</category>
<category>Network</category>
</categories>
<keywords>
<keyword>karaoke</keyword>
<keyword>music</keyword>
<keyword>video</keyword>
<keyword>audio</keyword>
<keyword>network</keyword>
<keyword>mpv</keyword>
<keyword>youtube</keyword>
</keywords>
<branding>
<color type="primary" scheme_preference="dark">#008000</color>
<color type="primary" scheme_preference="light">#67bf37</color>
</branding>
<launchable type="desktop-id">rocks.syng.Syng.desktop</launchable>
</component>

View file

@ -0,0 +1,407 @@
id: rocks.syng.Syng
runtime: org.kde.Platform
runtime-version: '6.7'
sdk: org.kde.Sdk
base: com.riverbankcomputing.PyQt.BaseApp
base-version: '6.7'
cleanup-commands:
- /app/cleanup-BaseApp.sh
build-options:
env:
- BASEAPP_REMOVE_WEBENGINE=1
finish-args:
- --env=QTWEBENGINEPROCESS_PATH=/app/bin/QtWebEngineProcess
# X11 + XShm access
- --socket=fallback-x11
- --share=ipc
- --socket=wayland
# Acceleration
- --device=dri
# Sound
- --socket=pulseaudio
# Playback files from anywhere on the system
- --filesystem=host:ro
- --share=network
cleanup:
- '*.la'
- '*.a'
command: syng
modules:
# MPV and MPV deps
# This is basically copied from the mpv flatpak
- name: libXmu
buildsystem: autotools
sources:
- type: git
url: https://gitlab.freedesktop.org/xorg/lib/libxmu.git
tag: libXmu-1.2.1
commit: 792f80402ee06ce69bca3a8f2a84295999c3a170
x-checker-data:
type: git
tag-pattern: ^libXmu-([\d.]+)$
- name: xclip
buildsystem: autotools
sources:
- type: git
url: https://github.com/astrand/xclip.git
tag: '0.13'
commit: 9aa7090c3b8b437c6489edca32ae43d82e0c1281
x-checker-data:
type: git
tag-pattern: ^(\d+\.\d+)$
- name: libXpresent
buildsystem: autotools
sources:
- type: git
url: https://gitlab.freedesktop.org/xorg/lib/libxpresent.git
tag: libXpresent-1.0.1
commit: 37507b5f44332accfb1064ee69a4f6a833994747
x-checker-data:
type: git
tag-pattern: ^libXpresent-([\d.]+)$
- name: luajit
no-autogen: true
cleanup:
- /bin
- /include
- /lib/pkgconfig
- /share/man
sources:
- type: git
url: https://github.com/LuaJIT/LuaJIT.git
disable-shallow-clone: true
commit: f5fd22203eadf57ccbaa4a298010d23974b22fc0
x-checker-data:
type: json
url: https://api.github.com/repos/LuaJIT/LuaJIT/commits
commit-query: first( .[].sha )
version-query: first( .[].sha )
timestamp-query: first( .[].commit.committer.date )
- type: shell
commands:
- sed -i 's|/usr/local|/app|' ./Makefile
- name: yt-dlp
no-autogen: true
no-make-install: true
make-args:
- yt-dlp
- PYTHON=/usr/bin/python3
post-install:
- install yt-dlp /app/bin
sources:
- type: archive
url: https://github.com/yt-dlp/yt-dlp/releases/download/2024.09.27/yt-dlp.tar.gz
sha256: ffce6ebd742373eff6dac89b23f706ec7513a0367160eb8b5a550cd706cd883f
x-checker-data:
type: json
url: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
version-query: .tag_name
url-query: .assets[] | select(.name=="yt-dlp.tar.gz") | .browser_download_url
- name: uchardet
buildsystem: cmake-ninja
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_STATIC=0
cleanup:
- /bin
- /include
- /lib/pkgconfig
- /share/man
sources:
- type: archive
url: https://www.freedesktop.org/software/uchardet/releases/uchardet-0.0.8.tar.xz
sha256: e97a60cfc00a1c147a674b097bb1422abd9fa78a2d9ce3f3fdcc2e78a34ac5f0
x-checker-data:
type: html
url: https://www.freedesktop.org/software/uchardet/releases/
version-pattern: uchardet-(\d\.\d+\.?\d*).tar.xz
url-template: https://www.freedesktop.org/software/uchardet/releases/uchardet-$version.tar.xz
- name: libass
cleanup:
- /include
- /lib/pkgconfig
config-opts:
- --disable-static
sources:
- type: git
url: https://github.com/libass/libass.git
tag: 0.17.3
commit: e46aedea0a0d17da4c4ef49d84b94a7994664ab5
x-checker-data:
type: git
tag-pattern: ^(\d\.\d{1,3}\.\d{1,2})$
- name: libaacs
config-opts:
- --disable-static
- --disable-bdjava-jar
cleanup:
- /include
- /lib/pkgconfig
sources:
- sha256: a88aa0ebe4c98a77f7aeffd92ab3ef64ac548c6b822e8248a8b926725bea0a39
type: archive
url: https://download.videolan.org/pub/videolan/libaacs/0.11.1/libaacs-0.11.1.tar.bz2
mirror-urls:
- https://videolan.mirror.ba/libaacs/0.11.1/libaacs-0.11.1.tar.bz2
- https://videolan.c3sl.ufpr.br/libaacs/0.11.1/libaacs-0.11.1.tar.bz2
x-checker-data:
type: html
url: https://www.videolan.org/developers/libaacs.html
version-pattern: Latest release is <b>libaacs (\d\.\d+\.?\d*)</b>
url-template: https://download.videolan.org/pub/videolan/libaacs/$version/libaacs-$version.tar.bz2
- name: zimg
config-opts:
- --disable-static
cleanup:
- /include
- /lib/pkgconfig
- /share/doc
sources:
- type: archive
archive-type: tar
url: https://api.github.com/repos/sekrit-twc/zimg/tarball/release-3.0.5
sha256: 1b8998f03f4a49e4d730033143722b32bc28a5306ef809ccfb3b4bbb29e4b784
x-checker-data:
type: json
url: https://api.github.com/repos/sekrit-twc/zimg/releases/latest
url-query: .tarball_url
version-query: .tag_name | sub("^release-"; "")
timestamp-query: .published_at
- name: mujs
buildsystem: autotools
no-autogen: true
make-args:
- release
make-install-args:
- prefix=/app
- install-shared
cleanup:
- /bin
- /include
- /lib/pkgconfig
sources:
- type: git
url: https://github.com/ccxvii/mujs.git
tag: 1.3.5
commit: 0df0707f2f10187127e36acfbc3ba9b9ca5b5cf0
x-checker-data:
type: git
url: https://api.github.com/repos/ccxvii/mujs/tags
tag-pattern: ^([\d.]+)$
- name: nv-codec-headers
cleanup:
- '*'
no-autogen: true
make-install-args:
- PREFIX=/app
sources:
- type: git
url: https://github.com/FFmpeg/nv-codec-headers.git
tag: n12.2.72.0
commit: c69278340ab1d5559c7d7bf0edf615dc33ddbba7
x-checker-data:
type: git
tag-pattern: ^n([\d.]+)$
- name: x264
cleanup:
- /include
- /lib/pkgconfig
- /share/man
config-opts:
- --disable-cli
- --enable-shared
sources:
- type: git
url: https://github.com/jpsdr/x264
commit: c24e06c2e184345ceb33eb20a15d1024d9fd3497
# Every commit to the master branch is considered a release
# https://code.videolan.org/videolan/x264/-/issues/35
x-checker-data:
type: json
url: https://code.videolan.org/api/v4/projects/536/repository/commits
commit-query: first( .[].id )
version-query: first( .[].id )
timestamp-query: first( .[].committed_date )
- name: x265
buildsystem: cmake
subdir: source
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_STATIC=0
cleanup:
- /include
- /lib/pkgconfig
- /share/man
sources:
- type: git
url: https://bitbucket.org/multicoreware/x265_git.git
tag: '4.0'
commit: 6318f223684118a2c71f67f3f4633a9e35046b00
x-checker-data:
type: git
tag-pattern: ^([\d.]+)$
- name: vulkan-headers
buildsystem: cmake-ninja
sources:
- type: archive
url: https://github.com/KhronosGroup/Vulkan-Headers/archive/v1.3.286.tar.gz
sha256: a82a6982efe5e603e23505ca19b469e8f3d876fc677c46b7bfb6177f517bf8fe
- name: ffmpeg
cleanup:
- /include
- /lib/pkgconfig
- /share/ffmpeg/examples
config-opts:
- --disable-debug
- --disable-doc
- --disable-static
- --enable-encoder=png
- --enable-gnutls
- --enable-gpl
- --enable-shared
- --enable-version3
- --enable-libaom
- --enable-libass
- --enable-libdav1d
- --enable-libfreetype
- --enable-libmp3lame
- --enable-libopus
- --enable-libtheora
- --enable-libvorbis
- --enable-libvpx
- --enable-libx264
- --enable-libx265
- --enable-libwebp
- --enable-libxml2
- --enable-vulkan
sources:
- type: git
url: https://github.com/FFmpeg/FFmpeg.git
commit: b08d7969c550a804a59511c7b83f2dd8cc0499b8
tag: n7.1
x-checker-data:
type: git
tag-pattern: ^n([\d.]{3,7})$
- name: libplacebo
buildsystem: meson
config-opts:
- -Dvulkan=enabled
- -Dshaderc=enabled
cleanup:
- /include
- /lib/pkgconfig
sources:
- type: git
url: https://github.com/haasn/libplacebo.git
tag: v7.349.0
commit: 1fd3c7bde7b943fe8985c893310b5269a09b46c5
x-checker-data:
type: git
tag-pattern: ^v([\d.]+)$
modules:
- name: shaderc
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DSHADERC_SKIP_COPYRIGHT_CHECK=ON
- -DSHADERC_SKIP_EXAMPLES=ON
- -DSHADERC_SKIP_TESTS=ON
- -DSPIRV_SKIP_EXECUTABLES=ON
- -DENABLE_GLSLANG_BINARIES=OFF
cleanup:
- /bin
- /include
- /lib/cmake
- /lib/pkgconfig
sources:
- type: git
url: https://github.com/google/shaderc.git
#tag: v2023.7
commit: 40bced4e1e205ecf44630d2dfa357655b6dabd04
#x-checker-data:
# type: git
# tag-pattern: ^v(\d{4}\.\d{1,2})$
- type: git
url: https://github.com/KhronosGroup/SPIRV-Tools.git
tag: v2024.1
commit: 04896c462d9f3f504c99a4698605b6524af813c1
dest: third_party/spirv-tools
#x-checker-data:
# type: git
# tag-pattern: ^v(\d{4}\.\d{1})$
- type: git
url: https://github.com/KhronosGroup/SPIRV-Headers.git
#tag: sdk-1.3.250.1
commit: 4f7b471f1a66b6d06462cd4ba57628cc0cd087d7
dest: third_party/spirv-headers
#x-checker-data:
# type: git
# tag-pattern: ^sdk-([\d.]+)$
- type: git
url: https://github.com/KhronosGroup/glslang.git
tag: 15.0.0
commit: 46ef757e048e760b46601e6e77ae0cb72c97bd2f
dest: third_party/glslang
x-checker-data:
type: git
tag-pattern: ^(\d{1,2}\.\d{1,2}\.\d{1,4})$
- name: mpv
buildsystem: meson
config-opts:
- -Dbuild-date=false
- -Dlibmpv=false
- -Dmanpage-build=disabled
- -Dlibarchive=enabled
- -Dsdl2=enabled
- -Dshaderc=disabled
- -Dvulkan=enabled
cleanup:
- /include
- /lib/pkgconfig
sources:
- type: git
url: https://github.com/mpv-player/mpv.git
tag: v0.39.0
commit: a0fba7be57f3822d967b04f0f6b6d6341e7516e7
x-checker-data:
type: git
tag-pattern: ^v([\d.]+)$
- python3-expandvars.yaml
- python3-cffi.yaml
- python3-requirements-client.yaml
- python3-poetry-core.yaml
- name: syng
buildsystem: simple
build-commands:
- pip install --prefix=/app --no-deps . --no-build-isolation
- install -Dm644 resources/${FLATPAK_ID}.desktop -t /app/share/applications
- install -Dm644 resources/flatpak/${FLATPAK_ID}.metainfo.xml -t /app/share/metainfo
- install -Dm644 resources/icons/hicolor/32x32/apps/${FLATPAK_ID}.png /app/share/icons/hicolor/32x32/apps/${FLATPAK_ID}.png
- install -Dm644 resources/icons/hicolor/48x48/apps/${FLATPAK_ID}.png /app/share/icons/hicolor/48x48/apps/${FLATPAK_ID}.png
- install -Dm644 resources/icons/hicolor/64x64/apps/${FLATPAK_ID}.png /app/share/icons/hicolor/64x64/apps/${FLATPAK_ID}.png
- install -Dm644 resources/icons/hicolor/128x128/apps/${FLATPAK_ID}.png /app/share/icons/hicolor/128x128/apps/${FLATPAK_ID}.png
- install -Dm644 resources/icons/hicolor/256x256/apps/${FLATPAK_ID}.png /app/share/icons/hicolor/256x256/apps/${FLATPAK_ID}.png
- install -Dm644 resources/icons/hicolor/512x512/apps/${FLATPAK_ID}.png /app/share/icons/hicolor/512x512/apps/${FLATPAK_ID}.png
# - install -Dm644 resources/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg /app/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
sources:
- type: git
url: https://github.com/christofsteel/syng.git
commit: dd84ff361bbd10efd14147d8dd0453438f4e32ff

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="eye_clear.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.80792474"
inkscape:cx="-106.44556"
inkscape:cy="176.99668"
inkscape:window-width="2560"
inkscape:window-height="1374"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<symbol
id="DreamSpeaking">
<title
id="title9">Dream Speaking</title>
<path
d="M 170,60 C 152,46 119,49 108,67 76,48 51,103 86,123 c -30,10 -30,50 3,57 -2,30 53,29 59,8 10,23 47,29 60,9 14,10 36,5 43,-9 11,25 41,21 50,1 35,4 40,-31 29,-50 24,-9 22,-39 3,-48 C 349,65 316,33 294,62 281,47.7 247,48 238,63 222,44 185,42 170,60 Z"
style="stroke:none"
id="path9" />
<path
d="m 160,180 c -33,35 58,-6 -53,96 57.1,-21 93,-56 111,-102"
style="stroke:none"
id="path10" />
<path
d="M 165,55 C 147,41 114,44 103,62 71,43 46,98 81,118 c -30,10 -30,50 3,57 -2,30 53,29 59,8 10,23 47,29 60,9 14,10 36,5 43,-9 11,25 41,21 50,1 35,4 40,-31 29,-50 24,-9 22,-39 3,-48 C 344,60 311,28 289,57 276,42.7 242,43 233,58 217,39 180,37 165,55 Z"
style="fill:#eeeeee;stroke-width:3.5"
id="path11" />
<path
d="m 155,176 c -33,35 58,-6 -53,96 57.1,-21 93,-56 111,-102"
style="fill:#eeeeee;stroke-width:3.5"
id="path12" />
<path
d="M 163,58 C 146,46 115,47 105,64 68,50 59,106 88,117 c -33,14 -20,48 2,50 -5,29 47,29 53,7 10,25 43,24 58,8 13,9 34,4 40,-8 10,22 41,19 48,1 31,7 37,-28 27,-43 23,-7 21,-38 3,-46 C 333,60 304,34 289,64 270,48.4 246,41 230,63 215,43 179,40 163,58 Z"
style="fill:#ffffff;stroke:none"
id="path13" />
<path
d="m 150,168 c -31,33 67,1 -38,97 56,-37 78,-67 93,-102"
style="fill:#ffffff;stroke:none"
id="path14" />
</symbol>
<symbol
id="station_solar"
viewBox="0 0 504.42743 512.16327">
<title
id="title6">Power station (solar)</title>
<path
d="M 347.55397,0.00113108 A 10.001,10.001 0 0 0 337.70436,10.143709 v 47.853516 a 10.001,10.001 0 1 0 20,0 V 10.143709 A 10.001,10.001 0 0 0 347.55397,0.00113108 Z M 277.99147,16.508944 a 10.001,10.001 0 0 0 -8.83204,14.757812 l 22.16211,42.414063 a 10.001,10.001 0 1 0 17.72461,-9.263672 L 286.88404,22.003084 a 10.001,10.001 0 0 0 -8.89257,-5.49414 z m 140.28711,1.736328 a 10.001,10.001 0 0 0 -8.59375,5.490234 l -22.16211,42.414063 a 10.001,10.001 0 1 0 17.72656,9.261718 L 427.41139,32.997225 A 10.001,10.001 0 0 0 418.27858,18.245272 Z M 221.75514,69.251131 a 10.001,10.001 0 0 0 -4.80664,18.578126 l 40.71875,25.142583 A 10.001,10.001 0 1 0 268.17506,95.954257 L 227.45631,70.811678 a 10.001,10.001 0 0 0 -5.70117,-1.560547 z m 252.75781,1.72461 a 10.001,10.001 0 0 0 -5.39844,1.566406 l -40.71875,25.14258 a 10.001,10.001 0 1 0 10.50782,17.017583 L 479.62233,89.559727 A 10.001,10.001 0 0 0 474.51295,70.975741 Z M 348.86647,82.727697 c -39.95383,0 -72.45118,32.497353 -72.45118,72.451173 0,39.95383 32.49735,72.44922 72.45118,72.44922 39.95382,0 72.44922,-32.49539 72.44922,-72.44922 0,-39.95382 -32.4954,-72.451173 -72.44922,-72.451173 z m -98.74805,62.359383 -47.85352,0.0918 a 10.001,10.001 0 1 0 0.0371,20 l 47.85547,-0.0918 a 10.001,10.001 0 1 0 -0.0391,-20 z m 244.14844,0.98437 -47.85352,0.0918 a 10.001,10.001 0 1 0 0.0371,20 l 47.85547,-0.0918 a 10.001,10.001 0 1 0 -0.0391,-20 z m -231.20117,50.64063 a 10.001,10.001 0 0 0 -5.39844,1.5664 l -40.71875,25.14258 a 10.001,10.001 0 1 0 10.50781,17.01562 l 40.71875,-25.14062 a 10.001,10.001 0 0 0 -5.10937,-18.58398 z m 170.13671,0.004 a 10.001,10.001 0 0 0 -4.80664,18.58008 l 40.71875,25.14062 a 10.001,10.001 0 1 0 10.50782,-17.01562 l -40.71875,-25.14258 a 10.001,10.001 0 0 0 -5.70118,-1.5625 z m -36.84765,35.35938 a 10.001,10.001 0 0 0 -8.83203,14.75586 l 22.16211,42.41406 a 10.001,10.001 0 1 0 17.72656,-9.26172 L 405.24928,237.5696 a 10.001,10.001 0 0 0 -8.89453,-5.49414 z m -96.44141,0.004 a 10.001,10.001 0 0 0 -8.5918,5.49024 l -22.16211,42.41386 a 10.001,10.001 0 1 0 17.72461,9.26172 l 22.16211,-42.41406 a 10.001,10.001 0 0 0 -9.13281,-14.75196 z m 48.80274,7.03125 a 10.001,10.001 0 0 0 -9.81836,9.28516 10.001,10.001 0 0 0 -1.19336,4.85547 v 47.85547 a 10.001,10.001 0 0 0 19.9707,0.85547 10.001,10.001 0 0 0 1.19141,-4.85547 V 249.25134 A 10.001,10.001 0 0 0 348.71608,239.11051 Z M 48.61061,360.05387 c -6.18495,-0.009 -12.106888,4.46701 -13.785156,10.41992 l -6.541016,23.06055 H 90.00123 l 4.10156,-33.48047 z m 69.36718,0 -4.10351,33.48047 h 73.43359 l 2.2168,-33.48047 z m 95.23633,0 -2.21875,33.48047 h 69.92188 l -2.21875,-33.48047 z m 89.17383,0 2.2168,33.48047 h 73.43359 l -4.10351,-33.48047 z m 95.42188,0 4.10156,33.48047 h 61.7168 l -6.54102,-23.06055 c -1.67827,-5.95291 -7.6002,-10.42799 -13.78516,-10.41992 z M 23.591082,412.45622 13.575457,447.93473 h 70.033202 l 4.35938,-35.47851 z m 88.251958,0 -4.36133,35.47851 h 76.38867 l 2.32813,-35.47851 z m 98.04492,0 -2.32812,35.47851 h 76.79296 l -2.32812,-35.47851 z m 95.82617,0 2.32813,35.47851 h 76.38867 l -4.36133,-35.47851 z m 98.23047,0 4.35938,35.47851 h 70.0332 L 468.32155,412.45622 Z M 7.514911,469.22184 0.530536,493.9445 c -1.20612,4.23593 -0.324707,9.01356 2.328125,12.52929 2.652834,3.51572 7.015632,5.68695 11.419922,5.68946 h 61.457031 l 5.28515,-42.94141 z m 97.343749,0 -5.248046,42.94141 h 80.009766 l 2.84571,-42.94141 z m 101.29688,0 -2.8457,42.94141 h 85.29296 l -2.8457,-42.94141 z m 103.29101,0 2.84571,42.94141 h 80.00976 l -5.24804,-42.94141 z m 101.44532,0 5.28515,42.94141 h 61.45703 c 4.40428,-0.002 8.7671,-2.17374 11.41993,-5.68946 2.65284,-3.51573 3.53424,-8.29336 2.32812,-12.52929 l -6.98437,-24.72266 z"
id="path7" />
</symbol>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path2"
style="fill:#ffffff;stroke:#000000;stroke-width:6.61458333;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
d="M 67.707723,31.564976 A 75.317235,75.317235 0 0 0 3.4889451,67.733563 75.317235,75.317235 0 0 0 67.707723,103.90169 75.317235,75.317235 0 0 0 131.97772,67.638091 75.317235,75.317235 0 0 0 67.707723,31.564976 Z" />
<circle
style="fill:#ffffff;stroke:#000000;stroke-width:6.61458333;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path3"
cx="67.73333"
cy="67.733337"
r="19.061646" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg1"
sodipodi:docname="eye_strike.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.80792474"
inkscape:cx="740.16795"
inkscape:cy="206.70242"
inkscape:window-width="2560"
inkscape:window-height="1374"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<symbol
id="DreamSpeaking">
<title
id="title9">Dream Speaking</title>
<path
d="M 170,60 C 152,46 119,49 108,67 76,48 51,103 86,123 c -30,10 -30,50 3,57 -2,30 53,29 59,8 10,23 47,29 60,9 14,10 36,5 43,-9 11,25 41,21 50,1 35,4 40,-31 29,-50 24,-9 22,-39 3,-48 C 349,65 316,33 294,62 281,47.7 247,48 238,63 222,44 185,42 170,60 Z"
style="stroke:none"
id="path9" />
<path
d="m 160,180 c -33,35 58,-6 -53,96 57.1,-21 93,-56 111,-102"
style="stroke:none"
id="path10" />
<path
d="M 165,55 C 147,41 114,44 103,62 71,43 46,98 81,118 c -30,10 -30,50 3,57 -2,30 53,29 59,8 10,23 47,29 60,9 14,10 36,5 43,-9 11,25 41,21 50,1 35,4 40,-31 29,-50 24,-9 22,-39 3,-48 C 344,60 311,28 289,57 276,42.7 242,43 233,58 217,39 180,37 165,55 Z"
style="fill:#eeeeee;stroke-width:3.5"
id="path11" />
<path
d="m 155,176 c -33,35 58,-6 -53,96 57.1,-21 93,-56 111,-102"
style="fill:#eeeeee;stroke-width:3.5"
id="path12" />
<path
d="M 163,58 C 146,46 115,47 105,64 68,50 59,106 88,117 c -33,14 -20,48 2,50 -5,29 47,29 53,7 10,25 43,24 58,8 13,9 34,4 40,-8 10,22 41,19 48,1 31,7 37,-28 27,-43 23,-7 21,-38 3,-46 C 333,60 304,34 289,64 270,48.4 246,41 230,63 215,43 179,40 163,58 Z"
style="fill:#ffffff;stroke:none"
id="path13" />
<path
d="m 150,168 c -31,33 67,1 -38,97 56,-37 78,-67 93,-102"
style="fill:#ffffff;stroke:none"
id="path14" />
</symbol>
<symbol
id="station_solar"
viewBox="0 0 504.42743 512.16327">
<title
id="title6">Power station (solar)</title>
<path
d="M 347.55397,0.00113108 A 10.001,10.001 0 0 0 337.70436,10.143709 v 47.853516 a 10.001,10.001 0 1 0 20,0 V 10.143709 A 10.001,10.001 0 0 0 347.55397,0.00113108 Z M 277.99147,16.508944 a 10.001,10.001 0 0 0 -8.83204,14.757812 l 22.16211,42.414063 a 10.001,10.001 0 1 0 17.72461,-9.263672 L 286.88404,22.003084 a 10.001,10.001 0 0 0 -8.89257,-5.49414 z m 140.28711,1.736328 a 10.001,10.001 0 0 0 -8.59375,5.490234 l -22.16211,42.414063 a 10.001,10.001 0 1 0 17.72656,9.261718 L 427.41139,32.997225 A 10.001,10.001 0 0 0 418.27858,18.245272 Z M 221.75514,69.251131 a 10.001,10.001 0 0 0 -4.80664,18.578126 l 40.71875,25.142583 A 10.001,10.001 0 1 0 268.17506,95.954257 L 227.45631,70.811678 a 10.001,10.001 0 0 0 -5.70117,-1.560547 z m 252.75781,1.72461 a 10.001,10.001 0 0 0 -5.39844,1.566406 l -40.71875,25.14258 a 10.001,10.001 0 1 0 10.50782,17.017583 L 479.62233,89.559727 A 10.001,10.001 0 0 0 474.51295,70.975741 Z M 348.86647,82.727697 c -39.95383,0 -72.45118,32.497353 -72.45118,72.451173 0,39.95383 32.49735,72.44922 72.45118,72.44922 39.95382,0 72.44922,-32.49539 72.44922,-72.44922 0,-39.95382 -32.4954,-72.451173 -72.44922,-72.451173 z m -98.74805,62.359383 -47.85352,0.0918 a 10.001,10.001 0 1 0 0.0371,20 l 47.85547,-0.0918 a 10.001,10.001 0 1 0 -0.0391,-20 z m 244.14844,0.98437 -47.85352,0.0918 a 10.001,10.001 0 1 0 0.0371,20 l 47.85547,-0.0918 a 10.001,10.001 0 1 0 -0.0391,-20 z m -231.20117,50.64063 a 10.001,10.001 0 0 0 -5.39844,1.5664 l -40.71875,25.14258 a 10.001,10.001 0 1 0 10.50781,17.01562 l 40.71875,-25.14062 a 10.001,10.001 0 0 0 -5.10937,-18.58398 z m 170.13671,0.004 a 10.001,10.001 0 0 0 -4.80664,18.58008 l 40.71875,25.14062 a 10.001,10.001 0 1 0 10.50782,-17.01562 l -40.71875,-25.14258 a 10.001,10.001 0 0 0 -5.70118,-1.5625 z m -36.84765,35.35938 a 10.001,10.001 0 0 0 -8.83203,14.75586 l 22.16211,42.41406 a 10.001,10.001 0 1 0 17.72656,-9.26172 L 405.24928,237.5696 a 10.001,10.001 0 0 0 -8.89453,-5.49414 z m -96.44141,0.004 a 10.001,10.001 0 0 0 -8.5918,5.49024 l -22.16211,42.41386 a 10.001,10.001 0 1 0 17.72461,9.26172 l 22.16211,-42.41406 a 10.001,10.001 0 0 0 -9.13281,-14.75196 z m 48.80274,7.03125 a 10.001,10.001 0 0 0 -9.81836,9.28516 10.001,10.001 0 0 0 -1.19336,4.85547 v 47.85547 a 10.001,10.001 0 0 0 19.9707,0.85547 10.001,10.001 0 0 0 1.19141,-4.85547 V 249.25134 A 10.001,10.001 0 0 0 348.71608,239.11051 Z M 48.61061,360.05387 c -6.18495,-0.009 -12.106888,4.46701 -13.785156,10.41992 l -6.541016,23.06055 H 90.00123 l 4.10156,-33.48047 z m 69.36718,0 -4.10351,33.48047 h 73.43359 l 2.2168,-33.48047 z m 95.23633,0 -2.21875,33.48047 h 69.92188 l -2.21875,-33.48047 z m 89.17383,0 2.2168,33.48047 h 73.43359 l -4.10351,-33.48047 z m 95.42188,0 4.10156,33.48047 h 61.7168 l -6.54102,-23.06055 c -1.67827,-5.95291 -7.6002,-10.42799 -13.78516,-10.41992 z M 23.591082,412.45622 13.575457,447.93473 h 70.033202 l 4.35938,-35.47851 z m 88.251958,0 -4.36133,35.47851 h 76.38867 l 2.32813,-35.47851 z m 98.04492,0 -2.32812,35.47851 h 76.79296 l -2.32812,-35.47851 z m 95.82617,0 2.32813,35.47851 h 76.38867 l -4.36133,-35.47851 z m 98.23047,0 4.35938,35.47851 h 70.0332 L 468.32155,412.45622 Z M 7.514911,469.22184 0.530536,493.9445 c -1.20612,4.23593 -0.324707,9.01356 2.328125,12.52929 2.652834,3.51572 7.015632,5.68695 11.419922,5.68946 h 61.457031 l 5.28515,-42.94141 z m 97.343749,0 -5.248046,42.94141 h 80.009766 l 2.84571,-42.94141 z m 101.29688,0 -2.8457,42.94141 h 85.29296 l -2.8457,-42.94141 z m 103.29101,0 2.84571,42.94141 h 80.00976 l -5.24804,-42.94141 z m 101.44532,0 5.28515,42.94141 h 61.45703 c 4.40428,-0.002 8.7671,-2.17374 11.41993,-5.68946 2.65284,-3.51573 3.53424,-8.29336 2.32812,-12.52929 l -6.98437,-24.72266 z"
id="path7" />
</symbol>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path2"
style="fill:#ffffff;stroke:#000000;stroke-width:6.61458333;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
d="M 67.707723,31.564976 A 75.317235,75.317235 0 0 0 3.4889451,67.733563 75.317235,75.317235 0 0 0 67.707723,103.90169 75.317235,75.317235 0 0 0 131.97772,67.638091 75.317235,75.317235 0 0 0 67.707723,31.564976 Z" />
<circle
style="fill:#ffffff;stroke:#000000;stroke-width:6.61458333;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path3"
cx="67.73333"
cy="67.733337"
r="19.061646" />
<rect
style="fill:#070707;fill-opacity:1;stroke:#fcfcfc;stroke-width:6.61458;stroke-dasharray:none;stroke-opacity:1"
id="rect4"
width="13.266691"
height="124.49303"
x="-6.6333461"
y="33.542885"
transform="rotate(-45)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 33.866666 33.866667"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="rocks.syng.gui2.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showguides="true"
inkscape:zoom="4.6965769"
inkscape:cx="68.241191"
inkscape:cy="55.146548"
inkscape:window-width="1920"
inkscape:window-height="1531"
inkscape:window-x="20"
inkscape:window-y="20"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<g
id="g21">
<circle
style="fill:#2ec27e;fill-opacity:1;stroke-width:15.5406"
id="circle21"
r="16.271875"
cy="16.933331"
cx="16.933334" />
</g>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<g
id="g22">
<circle
style="fill:#2ec27e;fill-opacity:1;stroke-width:15.5406"
id="circle22"
r="16.271875"
cy="16.933331"
cx="16.933334" />
</g>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath22">
<g
id="g23">
<circle
style="fill:#2ec27e;fill-opacity:1;stroke-width:15.5406"
id="circle23"
r="16.271875"
cy="16.933331"
cx="16.933334" />
</g>
</clipPath>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
inkscape:connector-curvature="0"
style="fill:#3d3846;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.854869;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 17.032165,8.0762396 -25.6168937,25.6168934 -0.107123,0.184519 c 0.1452355,0.24999 0.2814119,0.502927 0.4380264,0.748928 0.2914499,0.457721 0.6019592,0.907496 0.9303629,1.347638 0.3283998,0.440177 0.6742854,0.870171 1.0363635,1.288374 0.3620357,0.418161 0.7398069,0.824008 1.1318977,1.216024 0.2769335,0.276839 0.5608148,0.546552 0.8510935,0.808656 0.4109825,0.371156 0.8343648,0.726653 1.2685567,1.065155 0.4342002,0.338532 0.8786706,0.659646 1.3317486,0.962144 0.3735855,0.249412 0.7556397,0.47743 1.13918684,0.700413 L -0.37373862,41.90412 25.243154,16.287229 Z"
id="rect4521"
clip-path="url(#clipPath22)" />
<path
style="fill:#26a269;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.767436;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10.313989,5.5070223 A 12.376422,12.376422 0 0 0 10.367929,22.955457 12.376422,12.376422 0 0 0 27.80963,23.007897 16.630816,11.941314 45 0 1 26.825988,22.88049 16.630816,11.941314 45 0 1 25.385808,22.554352 16.630816,11.941314 45 0 1 23.93871,22.089311 16.630816,11.941314 45 0 1 22.499094,21.489478 16.630816,11.941314 45 0 1 21.08135,20.761399 16.630816,11.941314 45 0 1 19.699688,19.911985 16.630816,11.941314 45 0 1 18.367939,18.94984 16.630816,11.941314 45 0 1 17.099382,17.884687 16.630816,11.941314 45 0 1 16.248286,17.076028 16.630816,11.941314 45 0 1 15.116392,15.860005 16.630816,11.941314 45 0 1 14.080026,14.571631 16.630816,11.941314 45 0 1 13.149663,13.223993 16.630816,11.941314 45 0 1 12.334743,11.830459 16.630816,11.941314 45 0 1 11.643118,10.404863 16.630816,11.941314 45 0 1 11.081889,8.9616 16.630816,11.941314 45 0 1 10.656669,7.5150653 16.630816,11.941314 45 0 1 10.371662,6.0795606 16.630816,11.941314 45 0 1 10.313989,5.5070223 Z"
id="path4528"
inkscape:connector-curvature="0"
clip-path="url(#clipPath21)" />
<path
style="fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.767436;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10.313527,5.5065594 a 16.630816,11.941314 45 0 0 0.05767,0.572538 16.630816,11.941314 45 0 0 0.285007,1.4355047 16.630816,11.941314 45 0 0 0.425223,1.4465347 16.630816,11.941314 45 0 0 0.561227,1.4432632 16.630816,11.941314 45 0 0 0.691627,1.425596 16.630816,11.941314 45 0 0 0.81492,1.393534 16.630816,11.941314 45 0 0 0.930361,1.347638 16.630816,11.941314 45 0 0 1.036365,1.288374 16.630816,11.941314 45 0 0 1.131895,1.216024 16.630816,11.941314 45 0 0 0.851096,0.808659 16.630816,11.941314 45 0 0 1.268554,1.065152 16.630816,11.941314 45 0 0 1.331751,0.962143 16.630816,11.941314 45 0 0 1.381662,0.849415 16.630816,11.941314 45 0 0 1.417744,0.728082 16.630816,11.941314 45 0 0 1.439617,0.599832 16.630816,11.941314 45 0 0 1.447094,0.465042 16.630816,11.941314 45 0 0 1.440181,0.326135 16.630816,11.941314 45 0 0 0.983644,0.12741 12.376422,12.376422 0 0 0 0.05964,-0.05403 l 0.0062,-0.0062 a 12.376422,12.376422 0 0 0 -0.01073,-17.5013436 12.376422,12.376422 0 0 0 -17.501246,0.00767 12.376422,12.376422 0 0 0 -0.04945,0.052998 z"
id="path4523"
inkscape:connector-curvature="0"
clip-path="url(#clipPath20)" />
<path
style="fill:#2ec27e;fill-opacity:1;stroke-width:7.9375"
d="M 14.96738,-22.579915 20.569667,8.5719967"
id="path15" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
resources/icons/syng.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

7
resources/resources.qrc Normal file
View file

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/">
<file>icons/eye_strike.svg</file>
<file>icons/eye_clear.svg</file>
<file>icons/syng.ico</file>
</qresource>
</RCC>

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Syng
Comment=An all-in-one karaoke player
Exec=syng
Icon=rocks.syng.Syng

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View file

@ -0,0 +1,411 @@
{\rtf1\ansi\deff3\adeflang1025
{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f4\froman\fprq2\fcharset0 Times New Roman;}{\f5\fswiss\fprq2\fcharset0 Arial;}{\f6\froman\fprq2\fcharset0 StarSymbol{\*\falt Arial Unicode MS};}{\f7\froman\fprq2\fcharset0 Courier New;}{\f8\froman\fprq2\fcharset0 Arial;}{\f9\froman\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}{\f10\froman\fprq2\fcharset0 Liberation Mono{\*\falt Courier New};}{\f11\fmodern\fprq1\fcharset128 Liberation Mono{\*\falt Courier New};}{\f12\fnil\fprq2\fcharset0 Times New Roman;}{\f13\fnil\fprq2\fcharset0 StarSymbol{\*\falt Arial Unicode MS};}{\f14\fnil\fprq2\fcharset0 Courier New;}{\f15\fnil\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f16\fnil\fprq2\fcharset0 Liberation Mono{\*\falt Courier New};}{\f17\fnil\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}}
{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}
{\stylesheet{\s0\snext0\dbch\af12\langfe1081\dbch\af15\afs24\alang1081\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Normal;}
{\s1\sbasedon56\snext1\dbch\af12\langfe255\dbch\af15\afs32\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs32\lang1033\b\kerning1 Titre 1;}
{\s2\sbasedon56\snext2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1 Titre 2;}
{\s3\sbasedon56\snext3\dbch\af12\langfe255\dbch\af15\afs28\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\b\kerning1 Titre 3;}
{\s4\sbasedon56\snext4\dbch\af12\langfe255\dbch\af15\afs23\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs23\lang1033\i\b\kerning1 Titre 4;}
{\s5\sbasedon56\snext5\dbch\af12\langfe255\dbch\af15\afs23\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs23\lang1033\b\kerning1 Titre 5;}
{\s6\sbasedon56\snext6\dbch\af12\langfe255\dbch\af15\afs21\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs21\lang1033\b\kerning1 Titre 6;}
{\*\cs15\snext15 Caract\u232\'e8res de num\u233\'e9rotation;}
{\*\cs16\snext16\dbch\af13\afs18\loch\f6\fs18 Puces;}
{\*\cs17\snext17\i Accentuation;}
{\*\cs18\snext18\b Accentuation forte;}
{\*\cs19\snext19\strike Strikeout;}
{\*\cs20\snext20\super Superscript;}
{\*\cs21\snext21\sub Subscript;}
{\*\cs22\snext22\i Citation;}
{\*\cs23\snext23\dbch\af14\loch\f7 Texte non proportionnel;}
{\*\cs24\snext24\cf9\ul\ulc0 Lien Internet;}
{\*\cs25\snext25 Caract\u232\'e8res de note de bas de page;}
{\*\cs26\snext26\super Ancre de note de bas de page;}
{\*\cs27\snext27 D\u233\'e9finition;}
{\*\cs28\snext28\langfe255\cf13\lang255\ul\ulc0 Lien Internet visit\u233\'e9;}
{\*\cs29\snext29\dbch\af13\loch\f3 ListLabel 1;}
{\*\cs30\snext30\dbch\af13 ListLabel 2;}
{\*\cs31\snext31\dbch\af13 ListLabel 3;}
{\*\cs32\snext32\dbch\af13 ListLabel 4;}
{\*\cs33\snext33\dbch\af13 ListLabel 5;}
{\*\cs34\snext34\dbch\af13 ListLabel 6;}
{\*\cs35\snext35\dbch\af13 ListLabel 7;}
{\*\cs36\snext36\dbch\af13 ListLabel 8;}
{\*\cs37\snext37\dbch\af13 ListLabel 9;}
{\*\cs38\snext38\dbch\af13\loch\f3 ListLabel 10;}
{\*\cs39\snext39\dbch\af13 ListLabel 11;}
{\*\cs40\snext40\dbch\af13 ListLabel 12;}
{\*\cs41\snext41\dbch\af13 ListLabel 13;}
{\*\cs42\snext42\dbch\af13 ListLabel 14;}
{\*\cs43\snext43\dbch\af13 ListLabel 15;}
{\*\cs44\snext44\dbch\af13 ListLabel 16;}
{\*\cs45\snext45\dbch\af13 ListLabel 17;}
{\*\cs46\snext46\dbch\af13 ListLabel 18;}
{\*\cs47\snext47\dbch\af13\loch\f3 ListLabel 19;}
{\*\cs48\snext48\dbch\af13 ListLabel 20;}
{\*\cs49\snext49\dbch\af13 ListLabel 21;}
{\*\cs50\snext50\dbch\af13 ListLabel 22;}
{\*\cs51\snext51\dbch\af13 ListLabel 23;}
{\*\cs52\snext52\dbch\af13 ListLabel 24;}
{\*\cs53\snext53\dbch\af13 ListLabel 25;}
{\*\cs54\snext54\dbch\af13 ListLabel 26;}
{\*\cs55\snext55\dbch\af13 ListLabel 27;}
{\s56\sbasedon0\snext57\dbch\af12\langfe255\dbch\af15\afs28\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f8\fs28\lang1033\kerning1 Titre;}
{\s57\sbasedon0\snext57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Corps de texte;}
{\s58\sbasedon57\snext58\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Liste;}
{\s59\sbasedon0\snext59\dbch\af12\langfe255\dbch\af15\afs24\ai\ql\nowidctlpar\hyphpar0\sb120\sa120\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 L\u233\'e9gende;}
{\s60\sbasedon0\snext60\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Index;}
{\s61\sbasedon59\snext61\dbch\af12\langfe255\dbch\af15\afs24\ai\ql\nowidctlpar\hyphpar0\sb120\sa120\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 TableCaption;}
{\s62\sbasedon59\snext62\dbch\af12\langfe255\dbch\af15\afs24\ai\ql\nowidctlpar\hyphpar0\sb120\sa120\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 FigureCaption;}
{\s63\sbasedon0\snext63\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Figure;}
{\s64\sbasedon63\snext64\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\keepn\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 FigureWithCaption;}
{\s65\sbasedon0\snext65\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\li567\ri567\lin567\rin567\fi0\sb144\sa144\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Citations;}
{\s66\sbasedon0\snext66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1 Texte pr\u233\'e9format\u233\'e9;}
{\s67\sbasedon0\snext67\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Definition Term;}
{\s68\sbasedon0\snext68\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\li720\ri0\lin720\rin0\fi0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Definition Definition;}
{\s69\sbasedon0\snext69\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\li43\ri43\lin43\rin43\fi0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Contenu de tableau;}
{\s70\sbasedon69\snext70\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\li43\ri43\lin43\rin43\fi0\ltrpar\cf0\loch\f4\fs24\lang1033\b\kerning1 Titre de tableau;}
{\s71\sbasedon0\snext71\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\li283\ri0\lin283\rin0\fi-283\ltrpar\cf0\loch\f4\fs20\lang1033\kerning1 Note de bas de page;}
{\s72\sbasedon0\snext72\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\tqc\tx4819\tqr\tx9638\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 En-t\u234\'eate et pied de page;}
{\s73\sbasedon0\snext73\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\tqc\tx4680\tqr\tx9360\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Pied de page;}
{\s74\sbasedon0\snext74\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb115\sa115\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Definition Term Tight;}
{\s75\sbasedon0\snext75\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\li720\ri0\lin720\rin0\fi0\sb0\sa0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 Definition Definition Tight;}
{\s76\sbasedon0\snext76\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\i\kerning1 Date;}
{\s77\sbasedon0\snext77\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\i\kerning1 Author;}
{\s78\sbasedon0\snext78\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb0\sa283\brdrb\brdrdb\brdrw5\brdrcf15\brsp0\ltrpar\cf0\loch\f4\fs12\lang1033\kerning1 Ligne horizontale;}
{\s79\sbasedon0\snext79\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1 First paragraph;}
{\s80\sbasedon56\snext80\dbch\af12\langfe255\dbch\af15\afs56\ab\qc\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f8\fs56\lang1033\b\kerning1 Titre principal;}
}{\*\listtable{\list\listtemplateid1
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li720}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li1080}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li1440}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li1800}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li2160}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li2520}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li2880}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li3240}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li3600}\listid1}
{\list\listtemplateid2
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li720}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li1080}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li1440}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li1800}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li2160}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li2520}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li2880}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li3240}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li3600}\listid2}
{\list\listtemplateid3
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li720}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li1080}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li1440}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li1800}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li2160}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li2520}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8226 ?;}{\levelnumbers;}\f18\fi-360\li2880}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8227 ?;}{\levelnumbers;}\f18\fi-360\li3240}
{\listlevel\levelnfc23\leveljc0\levelstartat1\levelfollow0{\leveltext \'01\u8259 ?;}{\levelnumbers;}\f18\fi-360\li3600}\listid3}
{\list\listtemplateid4
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}
{\listlevel\levelnfc255\leveljc0\levelstartat1\levelfollow2{\leveltext \'00;}{\levelnumbers;}\fi0\li0}\listid4}
}{\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}{\listoverride\listid4\listoverridecount0\ls4}}{\*\generator LibreOffice/7.0.4.2$Linux_X86_64 LibreOffice_project/00$Build-2}{\info{\creatim\yr0\mo0\dy0\hr0\min0}{\revtim\yr2024\mo3\dy19\hr18\min2}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab709
\hyphauto1\viewscale100
{\*\pgdsctbl
{\pgdsc0\pgdscuse451\pgwsxn12240\pghsxn15840\marglsxn1440\margrsxn1440\margtsxn1440\margbsxn2016\footery1440{\footer\pard\plain \s73\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\tqc\tx4680\tqr\tx9360\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\qc\nowidctlpar\tqc\tx4680\tqr\tx9360\hyphpar0\ltrpar{\rtlch\dbch\af12\langfe255\afs24 \ltrch\cf0\fs24\lang1033\kerning1
{\field{\*\fldinst PAGE }{\fldrslt 12}}}{\rtlch\dbch\af12\langfe255\afs24 \ltrch\cf0\fs24\lang1033\kerning1
}
\par }\pgdscnxt0 Style de page par d\u233\'e9faut;}}
\formshade{\*\pgdscno0}\paperh15840\paperw12240\margl1440\margr1440\margt1440\margb1440\sectd\sbknone\pgndec\sftnnar\saftnnrlc\sectunlocked1\pgwsxn12240\pghsxn15840\marglsxn1440\margrsxn1440\margtsxn1440\margbsxn2016\footery1440{\footer\pard\plain \s73\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\tqc\tx4680\tqr\tx9360\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\qc\nowidctlpar\tqc\tx4680\tqr\tx9360\hyphpar0\ltrpar{\rtlch\dbch\af12\langfe255\afs24 \ltrch\cf0\fs24\lang1033\kerning1
{\field{\*\fldinst PAGE }{\fldrslt 12}}}{\rtlch\dbch\af12\langfe255\afs24 \ltrch\cf0\fs24\lang1033\kerning1
}
\par }\ftnbj\ftnstart1\ftnrstcont\ftnnar\aenddoc\aftnrstcont\aftnstart1\aftnnrlc
{\*\ftnsep\chftnsep}\pgndec\pard\plain \s80\dbch\af12\langfe255\dbch\af15\afs56\ab\qc\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f8\fs56\lang1033\b\kerning1\ql\sb240\sa120{\rtlch\dbch\af17\afs36\hich\af9 \ltrch\fs36\loch\f9\loch
GNU AFFERO GENERAL PUBLIC LICENSE}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\ql{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Version 3, 19 November 2007}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af11 \ltrch\loch\f11\loch
Copyright (C) 2007 Free Software Foundation, Inc. }{{\field{\*\fldinst HYPERLINK "https://fsf.org/" }{\fldrslt {\rtlch\dbch\af15\dbch\af15\hich\af11 \ltrch\cf9\ul\ulc0\loch\f11\loch
https://fsf.org/}}}}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af11 \ltrch\loch\f11\loch
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.}
\par \pard\plain \s1\dbch\af12\langfe255\dbch\af15\afs32\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs32\lang1033\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
Preamble}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The precise terms and conditions for copying, distribution and modification follow.}
\par \pard\plain \s1\dbch\af12\langfe255\dbch\af15\afs32\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs32\lang1033\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
TERMS AND CONDITIONS}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
0. Definitions.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
"This License" refers to version 3 of the GNU Affero General Public License.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A "covered work" means either the unmodified Program or a work based on the Program.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
1. Source Code.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The Corresponding Source for a work in source code form is that same work.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
2. Basic Permissions.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
3. Protecting Users' Legal Rights From Anti-Circumvention Law.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
4. Conveying Verbatim Copies.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
5. Conveying Modified Source Versions.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls1 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls1 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls1 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls1 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.}
\par \pard\plain \s79\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
6. Conveying Non-Source Forms.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls2 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls2 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls2 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls2 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls2 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.}
\par \pard\plain \s79\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
7. Additional Terms.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\listtext\pard\plain \u8226\'95\tab}\ilvl0\ls3 \li1440\ri0\lin1440\rin0\fi-360\tx720\li720\ri0\lin720\rin0\fi-360\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.}
\par \pard\plain \s79\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\sb85\sa85{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
8. Termination.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
9. Acceptance Not Required for Having Copies.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
10. Automatic Licensing of Downstream Recipients.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
11. Patents.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
12. No Surrender of Others' Freedom.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
13. Remote Network Interaction; Use with the GNU General Public License.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
14. Revised Versions of this License.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
15. Disclaimer of Warranty.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
16. Limitation of Liability.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.}
\par \pard\plain \s2\dbch\af12\langfe255\dbch\af15\afs28\ai\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs28\lang1033\i\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
17. Interpretation of Sections 15 and 16.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\ql{\rtlch\dbch\af15\ab\hich\af3 \ltrch\b\loch\f3\loch
END OF TERMS AND CONDITIONS}
\par \pard\plain \s1\dbch\af12\langfe255\dbch\af15\afs32\ab\ql\nowidctlpar\hyphpar0\sb240\sa120\keepn\ltrpar\cf0\loch\f5\fs32\lang1033\b\kerning1{\rtlch\dbch\af17\hich\af9 \ltrch\loch\f9\loch
How to Apply These Terms to Your New Programs}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\sb85\sa0{\rtlch\dbch\af16\afs20\hich\af10 \ltrch\fs20\loch\f10
}{\rtlch\dbch\af16\afs20\hich\af10 \ltrch\fs20\loch\f10\loch
<one line to give the program's name and a brief idea of what it does.>}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
Copyright (C) <year> <name of author>}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
This program is free software: you can redistribute it and/or modify}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
it under the terms of the GNU Affero General Public License as}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
published by the Free Software Foundation, either version 3 of the}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
License, or (at your option) any later version.}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
This program is distributed in the hope that it will be useful,}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
but WITHOUT ANY WARRANTY; without even the implied warranty of}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
GNU Affero General Public License for more details.}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
You should have received a copy of the GNU Affero General Public License}
\par \pard\plain \s66\dbch\af12\langfe255\dbch\af15\afs20\ql\nowidctlpar\hyphpar0\sb0\sa0\ltrpar\cf0\loch\f7\fs20\lang1033\kerning1\sb0\sa85{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10
}{\rtlch\dbch\af16\dbch\af16\hich\af10 \ltrch\loch\f10\loch
along with this program. If not, see <https://www.gnu.org/licenses/>.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\sb85\sa85{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
Also add information on how to contact you by electronic and paper mail.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1{\rtlch\dbch\af15\hich\af3 \ltrch\loch\f3\loch
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.}
\par \pard\plain \s57\dbch\af12\langfe255\dbch\af15\afs24\ql\nowidctlpar\hyphpar0\sb86\sa86\ltrpar\cf0\loch\f4\fs24\lang1033\kerning1\sb86\sa86{\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see }{{\field{\*\fldinst HYPERLINK "https://www.gnu.org/licenses/" }{\fldrslt {\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\cf9\ul\ulc0\loch\f3\loch
http}{}}}{\field{\*\fldinst HYPERLINK "https://www.gnu.org/licenses/" }{\fldrslt {\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\cf9\ul\ulc0\loch\f3\loch
s}{}}}{\field{\*\fldinst HYPERLINK "https://www.gnu.org/licenses/" }{\fldrslt {\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\cf9\ul\ulc0\loch\f3\loch
://www.gnu.org/licenses/}{}}}\rtlch\dbch\af15\dbch\af15\hich\af3 \ltrch\loch\f3\loch
.}
\par }

View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
mkdir -p src
mkdir -p requirements
cd requirements
# download mpv
# wget https://nightly.link/mpv-player/mpv/workflows/build/master/mpv-x86_64-windows-msvc.zip
# unzip mpv-x86_64-windows-msvc.zip
# cp mpv.exe ../src
# cp vulkan-1.dll ../src
wget https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20241118/mpv-dev-x86_64-20241118-git-e8fd7b8.7z
7z x mpv-dev-x86_64-20241118-git-e8fd7b8.7z
cp libmpv-2.dll ../src
# download ffmpeg
wget https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z
7z x ffmpeg-release-full.7z
cp ffmpeg-7.1-full_build/bin/ffmpeg.exe ../src
cd ..
rm -rf requirements
cp ../../requirements-client.txt src/requirements.txt
cp -r ../../syng/ src/
cp ../icons/syng.ico src/
# docker run --volume "$(pwd)/src:/src/" batonogov/pyinstaller-linux:latest "pyinstaller --onefile syng/main.py"
# rm -rf src/build
# rm -rf src/dist
# docker run --volume "$(pwd)/src:/src/" batonogov/pyinstaller-windows:latest "pyinstaller --onefile -w -i'.\syng.ico' --add-data='.\syng\static\syng.png;.\static' --add-binary '.\mpv.exe;.' --add-binary '.\vulkan-1.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py"
docker run --volume "$(pwd)/src:/src/" batonogov/pyinstaller-windows:latest "pyinstaller -w -i'.\syng.ico' --add-data='.\syng.ico;.' --add-binary '.\libmpv-2.dll;.' --add-binary '.\ffmpeg.exe;.' syng/main.py"
# cd syng-2.0.1
# wine python -m poetry install -E client
# wine poetry run pyinstaller -w syng/main.py
# cp -rv build /out
# cp -rv dist /out

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Language="1033"
Manufacturer="Syng.Rocks!"
Name="Syng.Rocks! Karaoke Player"
Scope="perUserOrMachine"
UpgradeCode="092e7e0b-5042-47a1-9673-544d9722f8df"
ProductCode="*"
Version="2.1.0">
<MediaTemplate EmbedCab="yes" />
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<WixVariable Id="WixUILicenseRtf" Value="agpl-3.0.rtf" />
<Icon Id="syng.ico" SourceFile="..\syng.ico"/>
<Property Id="ARPPRODUCTICON" Value="syng.ico" />
<StandardDirectory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="syng">
<Component Id="ProductComponent">
<File KeyPath="yes" Source="syng\syng.exe" Name="syng.exe"></File>
<Shortcut Id="startmenuShortcut"
Directory="ProgramMenuDir"
Name="Syng.Rocks! Karaoke Player"
WorkingDirectory='INSTALLFOLDER'
Icon="syng.ico"
IconIndex="0"
Advertise="yes" />
<Shortcut Id="UninstallProduct"
Name="Uninstall Syng.Rocks! Karaoke Player"
Target="[SystemFolder]msiexec.exe"
Arguments="/x [ProductCode]"
Description="Uninstalls Syng" />
<Shortcut Id="desktopShortcut"
Directory="DesktopFolder"
Name="Syng.Rocks! Karaoke Player"
WorkingDirectory='INSTALLFOLDER'
Icon="syng.ico"
IconIndex="0"
Advertise="yes" />
</Component>
<Directory Id="DataDir" Name="data">
</Directory>
</Directory>
</StandardDirectory>
<ComponentGroup Id="DataFiles" Directory="DataDir">
<Files Include="syng\data\**">
<Exclude Files="syng\syng.exe" />
</Files>
</ComponentGroup>
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="ProgramMenuDir" Name="syng"/>
</StandardDirectory>
<StandardDirectory Id="DesktopFolder"/>
<Feature Id="syng">
<ComponentRef Id="ProductComponent" />
<ComponentGroupRef Id="DataFiles" />
</Feature></Package>
</Wix>

View file

@ -1,3 +0,0 @@
class aiocmd:
class PromptToolkitCmd:
async def run(self) -> None: ...

View file

@ -1,6 +0,0 @@
class Info:
length: int
class File:
def __init__(self, filename: str): ...
info: Info

View file

@ -1,60 +0,0 @@
from __future__ import annotations
from collections.abc import Iterable
from typing import Any, Callable, Iterator, Optional
class exceptions:
class PytubeError(Exception): ...
class Channel:
channel_id: str
def __init__(self, url: str) -> None:
pass
class innertube:
class InnerTube:
base_url: str
base_data: dict[str, str]
base_params: dict[str, str]
def _call_api(
self, endpoint: str, params: dict[str, str], data: dict[str, str]
) -> dict[str, Any]: ...
def __init__(self, client: str) -> None: ...
class Stream:
resolution: str
is_progressive: bool
is_adaptive: bool
abr: str
def download(
self,
output_path: Optional[str] = None,
filename_prefix: Optional[str] = None,
) -> str: ...
class StreamQuery(Iterable[Stream]):
resolution: str
def filter(
self,
type: Optional[str] = None,
custom_filter_functions: Optional[
list[Callable[[StreamQuery], bool]]
] = None,
only_audio: bool = False,
) -> StreamQuery: ...
def __iter__(self) -> Iterator[Stream]: ...
class YouTube:
def __init__(self, url: str) -> None: ...
length: int
title: str
author: str
watch_url: str
streams: StreamQuery
class Search:
results: Optional[list[YouTube]]
def __init__(self, query: str) -> None: ...

View file

@ -0,0 +1,2 @@
SYNG_VERSION = (2, 1, 1)
SYNG_PROTOCOL_VERSION = (2, 1, 1)

4
syng/__main__.py Normal file
View file

@ -0,0 +1,4 @@
from .main import main
if __name__ == "__main__":
main()

View file

@ -1,79 +1,71 @@
""" """
Module for the playback client. Module for the playback client.
Excerp from the help:: The client connects to the server via the socket.io protocol, and plays the
songs, that are sent by the server.
usage: client.py [-h] [--room ROOM] [--secret SECRET] \
[--config-file CONFIG_FILE] [--server server]
options:
-h, --help show this help message and exit
--room ROOM, -r ROOM
--secret SECRET, -s SECRET
--config-file CONFIG_FILE, -C CONFIG_FILE
--key KEY, -k KEY
--server
The config file should be a yaml file in the following style::
sources:
SOURCE1:
configuration for SOURCE
SOURCE2:
configuration for SOURCE
...
config:
server: ...
room: ...
preview_duration: ...
secret: ...
last_song: ...
waiting_room_policy: ..
Playback is done by the :py:class:`syng.sources.source.Source` objects, that
are configured in the `sources` section of the configuration file and can currently
be one of:
- `youtube`
- `s3`
- `files`
""" """
import asyncio
import datetime from __future__ import annotations
from collections.abc import Callable
import logging import logging
import os import os
import asyncio
import datetime
from logging import LogRecord
from logging.handlers import QueueHandler
from multiprocessing import Queue
import secrets import secrets
import string import string
import tempfile
import signal import signal
from argparse import ArgumentParser from argparse import Namespace
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field from dataclasses import field
from traceback import print_exc from traceback import print_exc
from typing import Any, Optional from typing import Any, Optional
import platformdirs from uuid import UUID
import qrcode from qrcode.main import QRCode
import socketio import socketio
from socketio.exceptions import ConnectionError, BadNamespaceError
import engineio import engineio
from PIL import Image
from yaml import load, Loader from yaml import load, Loader
from . import jsonencoder from syng.player_libmpv import Player, QRPosition
from . import SYNG_VERSION, jsonencoder
from .entry import Entry from .entry import Entry
from .sources import configure_sources, Source from .sources import configure_sources, Source
from .log import logger
sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder)
logger: logging.Logger = logging.getLogger(__name__)
sources: dict[str, Source] = {}
currentLock: asyncio.Semaphore = asyncio.Semaphore(0)
def default_config() -> dict[str, Optional[int | str]]: def default_config() -> dict[str, Optional[int | str]]:
"""
Return a default configuration for the client.
:returns: A dictionary with the default configuration.
:rtype: dict[str, Optional[int | str]]
"""
return { return {
"server": "http://localhost:8080", "server": "https://syng.rocks",
"room": "ABCD", "room": "",
"preview_duration": 3, "preview_duration": 3,
"secret": None, "secret": None,
"last_song": None, "last_song": None,
"waiting_room_policy": None, "waiting_room_policy": None,
"key": None,
"buffer_in_advance": 2,
"qr_box_size": 5,
"qr_position": "bottom-right",
"show_advanced": False,
"log_level": "info",
} }
@ -100,7 +92,8 @@ class State:
* `secret` (`str`): The passcode of the room. If a playback client reconnects to * `secret` (`str`): The passcode of the room. If a playback client reconnects to
a room, this must be identical. Also, if a webclient wants to have a room, this must be identical. Also, if a webclient wants to have
admin privileges, this must be included. admin privileges, this must be included.
* `key` (`Optional[str]`) An optional key, if registration on the server is limited. * `key` (`Optional[str]`) An optional key, if registration or functionality on the server
is limited.
* `preview_duration` (`Optional[int]`): The duration in seconds the * `preview_duration` (`Optional[int]`): The duration in seconds the
playback client shows a preview for the next song. This is accounted for playback client shows a preview for the next song. This is accounted for
in the calculation of the ETA for songs later in the queue. in the calculation of the ETA for songs later in the queue.
@ -112,6 +105,19 @@ class State:
- `optional`, if a performer is already in the queue, they have the option - `optional`, if a performer is already in the queue, they have the option
to be put in the waiting room. to be put in the waiting room.
- `None`, performers are always added to the queue. - `None`, performers are always added to the queue.
* `buffer_in_advance` (`int`): The number of songs, that are buffered in
advance.
* `qr_box_size` (`int`): The size of one box in the QR code.
* `qr_position` (`str`): The position of the QR code on the screen. One of:
- `top-left`
- `top-right`
- `bottom-left`
- `bottom-right`
* `show_advanced` (`bool`): If the advanced options should be shown in the
gui.
* `log_level` (`str`): The log level of the client. One of: `debug`, `info`, `warning`,
`error`, `critical`. Default is `info`.
:type config: dict[str, Any]: :type config: dict[str, Any]:
""" """
@ -124,16 +130,98 @@ class State:
config: dict[str, Any] = field(default_factory=default_config) config: dict[str, Any] = field(default_factory=default_config)
state: State = State() class Client:
def __init__(self, config: dict[str, Any]):
config["config"] = default_config() | config["config"]
self.is_running = False
self.is_quitting = False
self.set_log_level(config["config"]["log_level"])
self.sio = socketio.AsyncClient(json=jsonencoder)
self.loop: Optional[asyncio.AbstractEventLoop] = None
self.skipped: list[UUID] = []
self.sources = configure_sources(config["sources"])
self.state = State()
self.currentLock = asyncio.Semaphore(0)
self.buffer_in_advance = config["config"]["buffer_in_advance"]
self.player = Player(
f"{config['config']['server']}/{config['config']['room']}",
1 if config["config"]["qr_box_size"] < 1 else config["config"]["qr_box_size"],
QRPosition.from_string(config["config"]["qr_position"]),
self.quit_callback,
)
self.register_handlers()
self.queue_callbacks: list[Callable[[list[Entry]], None]] = []
@sio.on("update_config") def add_queue_callback(self, callback: Callable[[list[Entry]], None]) -> None:
async def handle_update_config(data: dict[str, Any]) -> None: self.queue_callbacks.append(callback)
state.config = default_config() | data
def set_log_level(self, level: str) -> None:
match level:
case "debug":
logger.setLevel(logging.DEBUG)
case "info":
logger.setLevel(logging.INFO)
case "warning":
logger.setLevel(logging.WARNING)
case "error":
logger.setLevel(logging.ERROR)
case "critical":
logger.setLevel(logging.CRITICAL)
@sio.on("skip-current") def register_handlers(self) -> None:
async def handle_skip_current(data: dict[str, Any]) -> None: self.sio.on("update_config", self.handle_update_config)
self.sio.on("skip-current", self.handle_skip_current)
self.sio.on("state", self.handle_state)
self.sio.on("connect", self.handle_connect)
self.sio.on("get-meta-info", self.handle_get_meta_info)
self.sio.on("play", self.handle_play)
self.sio.on("search", self.handle_search)
self.sio.on("client-registered", self.handle_client_registered)
self.sio.on("request-config", self.handle_request_config)
self.sio.on("msg", self.handle_msg)
self.sio.on("disconnect", self.handle_disconnect)
async def handle_disconnect(self) -> None:
logger.info("Disconnected from server")
async def handle_msg(self, data: dict[str, Any]) -> None:
"""
Handle the "msg" message.
This function is used to print messages from the server to the console.
:param data: A dictionary with the `msg` entry.
:type data: dict[str, Any]
:rtype: None
"""
msg_type = data.get("type", "info")
match msg_type:
case "debug":
logger.debug(data["msg"])
case "info":
logger.info(data["msg"])
case "warning":
logger.warning(data["msg"])
case "error":
logger.error(data["msg"])
case "critical":
logger.critical(data["msg"])
async def handle_update_config(self, data: dict[str, Any]) -> None:
"""
Handle the "update_config" message.
Currently, this function is untested and should be considered dangerous.
:param data: A dictionary with the new configuration.
:type data: dict[str, Any]
:rtype: None
"""
self.state.config = default_config() | data
async def handle_skip_current(self, data: dict[str, Any]) -> None:
""" """
Handle the "skip-current" message. Handle the "skip-current" message.
@ -148,12 +236,16 @@ async def handle_skip_current(data: dict[str, Any]) -> None:
:rtype: None :rtype: None
""" """
logger.info("Skipping current") logger.info("Skipping current")
if state.current_source is not None: self.skipped.append(data["uuid"])
await state.current_source.skip_current(Entry(**data))
entry = Entry(**data)
logger.info("Skipping: %s", entry.title)
source = self.sources[entry.source]
@sio.on("state") await source.skip_current(Entry(**data))
async def handle_state(data: dict[str, Any]) -> None: self.player.skip_current()
async def handle_state(self, data: dict[str, Any]) -> None:
""" """
Handle the "state" message. Handle the "state" message.
@ -168,17 +260,31 @@ async def handle_state(data: dict[str, Any]) -> None:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
state.queue = [Entry(**entry) for entry in data["queue"]] self.state.queue.clear()
state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]] self.state.queue.extend([Entry(**entry) for entry in data["queue"]])
state.recent = [Entry(**entry) for entry in data["recent"]] # self.state.queue = [Entry(**entry) for entry in data["queue"]]
self.state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]]
self.state.recent = [Entry(**entry) for entry in data["recent"]]
for entry in state.queue[:2]: for pos, entry in enumerate(self.state.queue[0 : self.buffer_in_advance]):
logger.info("Buffering: %s", entry.title) source = self.sources[entry.source]
await sources[entry.source].buffer(entry) if entry.incomplete_data:
meta_info = await source.get_missing_metadata(entry)
await self.sio.emit("meta-info", {"uuid": entry.uuid, "meta": meta_info})
entry.update(**meta_info)
if entry.ident in source.downloaded_files:
continue
logger.info("Buffering: %s (%d s)", entry.title, entry.duration)
try:
await self.sources[entry.source].buffer(entry, pos)
except ValueError as e:
logger.error("Error buffering: %s", e)
await self.sio.emit("skip", {"uuid": entry.uuid})
for callback in self.queue_callbacks:
callback(self.state.queue)
@sio.on("connect") async def handle_connect(self) -> None:
async def handle_connect() -> None:
""" """
Handle the "connect" message. Handle the "connect" message.
@ -196,18 +302,17 @@ async def handle_connect() -> None:
:rtype: None :rtype: None
""" """
logging.info("Connected to server") logger.info("Connected to server")
data = { data = {
"queue": state.queue, "queue": self.state.queue,
"waiting_room": state.waiting_room, "waiting_room": self.state.waiting_room,
"recent": state.recent, "recent": self.state.recent,
"config": state.config, "config": self.state.config,
"version": SYNG_VERSION,
} }
await sio.emit("register-client", data) await self.sio.emit("register-client", data)
async def handle_get_meta_info(self, data: dict[str, Any]) -> None:
@sio.on("get-meta-info")
async def handle_get_meta_info(data: dict[str, Any]) -> None:
""" """
Handle a "get-meta-info" message. Handle a "get-meta-info" message.
@ -219,12 +324,11 @@ async def handle_get_meta_info(data: dict[str, Any]) -> None:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
source: Source = sources[data["source"]] source: Source = self.sources[data["source"]]
meta_info: dict[str, Any] = await source.get_missing_metadata(Entry(**data)) meta_info: dict[str, Any] = await source.get_missing_metadata(Entry(**data))
await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info}) await self.sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info})
async def preview(self, entry: Entry) -> None:
async def preview(entry: Entry) -> None:
""" """
Generate and play a preview for a given :py:class:`Entry`. Generate and play a preview for a given :py:class:`Entry`.
@ -238,29 +342,9 @@ async def preview(entry: Entry) -> None:
:type entry: :py:class:`Entry` :type entry: :py:class:`Entry`
:rtype: None :rtype: None
""" """
background = Image.new("RGB", (1280, 720)) await self.player.queue_next(entry)
subtitle: str = f"""1
00:00:00,00 --> 00:05:00,00
{entry.artist} - {entry.title}
{entry.performer}"""
with tempfile.NamedTemporaryFile() as tmpfile:
background.save(tmpfile, "png")
process = await asyncio.create_subprocess_exec(
"mpv",
tmpfile.name,
f"--image-display-duration={state.config['preview_duration']}",
"--sub-pos=50",
"--sub-file=-",
"--fullscreen",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await process.communicate(subtitle.encode())
async def handle_play(self, data: dict[str, Any]) -> None:
@sio.on("play")
async def handle_play(data: dict[str, Any]) -> None:
""" """
Handle the "play" message. Handle the "play" message.
@ -280,26 +364,64 @@ async def handle_play(data: dict[str, Any]) -> None:
:rtype: None :rtype: None
""" """
entry: Entry = Entry(**data) entry: Entry = Entry(**data)
source = self.sources[entry.source]
print( print(
f"Playing: {entry.artist} - {entry.title} [{entry.album}] " f"Playing: {entry.artist} - {entry.title} [{entry.album}] "
f"({entry.source}) for {entry.performer}" f"({entry.source}) for {entry.performer}"
) )
if entry.uuid not in self.skipped:
try: try:
state.current_source = sources[entry.source] if self.state.config["preview_duration"] > 0:
if state.config["preview_duration"] > 0: await self.preview(entry)
await preview(entry) video, audio = await source.ensure_playable(entry)
await sources[entry.source].play(entry) if entry.uuid not in self.skipped:
self.skipped = []
await self.player.play(video, audio, source.extra_mpv_options)
except ValueError as e:
logger.error("Error playing: %s", e)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
print_exc() print_exc()
state.current_source = None if self.skipped:
if entry.skip: self.skipped.remove(entry.uuid)
await sio.emit("get-first") await self.sio.emit("get-first")
else: else:
await sio.emit("pop-then-get-next") try:
await self.sio.emit("pop-then-get-next")
except BadNamespaceError:
pass
async def handle_search(self, data: dict[str, Any]) -> None:
"""
Handle the "search" message.
@sio.on("client-registered") This handles client side search requests. It sends a search request to all
async def handle_client_registered(data: dict[str, Any]) -> None: configured :py:class:`syng.sources.source.Source` and collects the results.
The results are then send back to the server in a "search-results" message,
including the `sid` of the corresponding webclient.
:param data: A dictionary with the `query` and `sid` entry.
:type data: dict[str, Any]
:rtype: None
"""
query = data["query"]
sid = data["sid"]
search_id = data["search_id"]
results_list = await asyncio.gather(
*[source.search(query) for source in self.sources.values()]
)
results = [
search_result.to_dict()
for source_result in results_list
for search_result in source_result
]
await self.sio.emit(
"search-results", {"results": results, "sid": sid, "search_id": search_id}
)
async def handle_client_registered(self, data: dict[str, Any]) -> None:
""" """
Handle the "client-registered" message. Handle the "client-registered" message.
@ -321,23 +443,31 @@ async def handle_client_registered(data: dict[str, Any]) -> None:
:rtype: None :rtype: None
""" """
if data["success"]: if data["success"]:
logging.info("Registered") self.player.start()
print(f"Join here: {state.config['server']}/{data['room']}")
qr = qrcode.QRCode(box_size=20, border=2) logger.info("Connected to room: %s", data["room"])
qr.add_data(f"{state.config['server']}/{data['room']}") qr_string = f"{self.state.config['server']}/{data['room']}"
self.player.update_qr(qr_string)
# this is borked on windows
await self.handle_state(data)
if os.name != "nt":
print(f"Join here: {self.state.config['server']}/{data['room']}")
qr = QRCode(box_size=20, border=2)
qr.add_data(qr_string)
qr.make() qr.make()
qr.print_ascii() qr.print_ascii()
state.config["room"] = data["room"]
await sio.emit("sources", {"sources": list(sources.keys())}) self.state.config["room"] = data["room"]
if state.current_source is None: # A possible race condition can occur here await self.sio.emit("sources", {"sources": list(self.sources.keys())})
await sio.emit("get-first") if self.state.current_source is None: # A possible race condition can occur here
await self.sio.emit("get-first")
else: else:
logging.warning("Registration failed") reason = data.get("reason", "Unknown")
await sio.disconnect() logger.critical(f"Registration failed: {reason}")
await self.sio.disconnect()
async def handle_request_config(self, data: dict[str, Any]) -> None:
@sio.on("request-config")
async def handle_request_config(data: dict[str, Any]) -> None:
""" """
Handle the "request-config" message. Handle the "request-config" message.
@ -346,39 +476,72 @@ async def handle_request_config(data: dict[str, Any]) -> None:
A Source can decide, that the config will be split up in multiple Parts. A Source can decide, that the config will be split up in multiple Parts.
If this is the case, multiple "config-chunk" messages will be send with a If this is the case, multiple "config-chunk" messages will be send with a
running enumerator. Otherwise a singe "config" message will be send. running enumerator. Otherwise a single "config" message will be send.
After the configuration is send, the source is asked to update its
configuration. This can also be split up in multiple parts.
:param data: A dictionary with the entry `source` and a string, that :param data: A dictionary with the entry `source` and a string, that
corresponds to the name of a source. corresponds to the name of a source.
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
if data["source"] in sources: if data["source"] in self.sources:
config: dict[str, Any] | list[dict[str, Any]] = await sources[data["source"]].get_config() config: dict[str, Any] | list[dict[str, Any]] = await self.sources[
data["source"]
].get_config()
if isinstance(config, list): if isinstance(config, list):
num_chunks: int = len(config) num_chunks: int = len(config)
for current, chunk in enumerate(config): for current, chunk in enumerate(config):
await sio.emit( await self.sio.emit(
"config-chunk", "config-chunk",
{ {
"source": data["source"], "source": data["source"],
"config": chunk, "config": chunk,
"number": current + 1, "number": current,
"total": num_chunks, "total": num_chunks,
}, },
) )
else: else:
await sio.emit("config", {"source": data["source"], "config": config}) await self.sio.emit("config", {"source": data["source"], "config": config})
updated_config = await self.sources[data["source"]].update_config()
if isinstance(updated_config, list):
num_chunks = len(updated_config)
for current, chunk in enumerate(updated_config):
await self.sio.emit(
"config-chunk",
{
"source": data["source"],
"config": chunk,
"number": current,
"total": num_chunks,
},
)
elif updated_config is not None:
await self.sio.emit("config", {"source": data["source"], "config": updated_config})
def signal_handler() -> None: def signal_handler(self) -> None:
"""
Signal handler for the client.
This function is called when the client receives a signal to terminate. It
will disconnect from the server and kill the current player.
:rtype: None
"""
engineio.async_client.async_signal_handler() engineio.async_client.async_signal_handler()
if state.current_source is not None: if self.player.mpv is not None:
if state.current_source.player is not None: self.player.mpv.terminate()
state.current_source.player.kill()
def quit_callback(self) -> None:
if self.is_quitting:
return
self.is_quitting = True
if self.loop is not None:
asyncio.run_coroutine_threadsafe(self.sio.disconnect(), self.loop)
async def start_client(config: dict[str, Any]) -> None: async def start_client(self, config: dict[str, Any]) -> None:
""" """
Initialize the client and connect to the server. Initialize the client and connect to the server.
@ -387,7 +550,9 @@ async def start_client(config: dict[str, Any]) -> None:
:rtype: None :rtype: None
""" """
sources.update(configure_sources(config["sources"])) self.loop = asyncio.get_running_loop()
self.sources.update(configure_sources(config["sources"]))
if "config" in config: if "config" in config:
last_song = ( last_song = (
@ -395,52 +560,77 @@ async def start_client(config: dict[str, Any]) -> None:
if "last_song" in config["config"] and config["config"]["last_song"] if "last_song" in config["config"] and config["config"]["last_song"]
else None else None
) )
state.config |= config["config"] | {"last_song": last_song} self.state.config |= config["config"] | {"last_song": last_song}
if not ("secret" in state.config and state.config["secret"]): if not ("secret" in self.state.config and self.state.config["secret"]):
state.config["secret"] = "".join( self.state.config["secret"] = "".join(
secrets.choice(string.ascii_letters + string.digits) for _ in range(8) secrets.choice(string.ascii_letters + string.digits) for _ in range(8)
) )
print(f"Generated secret: {state.config['secret']}") print(f"Generated secret: {self.state.config['secret']}")
if not ("key" in state.config and state.config["key"]): if not ("key" in self.state.config and self.state.config["key"]):
state.config["key"] = "" self.state.config["key"] = ""
await sio.connect(state.config["server"])
asyncio.get_event_loop().add_signal_handler(signal.SIGINT, signal_handler)
asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, signal_handler)
try: try:
await sio.wait() await self.sio.connect(self.state.config["server"])
# this is not supported under windows
if os.name != "nt":
asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self.signal_handler)
self.is_running = True
await self.sio.wait()
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except ConnectionError:
logger.critical("Could not connect to server")
finally: finally:
if state.current_source is not None: self.is_running = False
if state.current_source.player is not None: if self.player.mpv is not None:
state.current_source.player.kill() self.player.mpv.terminate()
def create_async_and_start_client(config: dict[str, Any]) -> None: def create_async_and_start_client(
asyncio.run(start_client(config)) config: dict[str, Any],
queue: Optional[Queue[LogRecord]] = None,
client: Optional[Client] = None,
) -> None:
"""
Create an asyncio event loop and start the client.
If a multiprocessing queue is given, the client will log to the queue.
:param config: Config options for the client
:type config: dict[str, Any]
:param queue: A multiprocessing queue to log to
:type queue: Optional[Queue[LogRecord]]
:rtype: None
"""
if queue is not None:
logger.addHandler(QueueHandler(queue))
if client is None:
client = Client(config)
asyncio.run(client.start_client(config))
def main() -> None: def run_client(args: Namespace) -> None:
"""Entry point for the syng-client script.""" """
parser: ArgumentParser = ArgumentParser() Run the client with the given arguments.
parser.add_argument("--room", "-r") Namespace contains the following attributes:
parser.add_argument("--secret", "-s") - room: The room code to connect to
parser.add_argument( - secret: The secret to connect to the room
"--config-file", - config_file: The path to the configuration file
"-C", - key: The key to connect to the server
default=f"{os.path.join(platformdirs.user_config_dir('syng'), 'config.yaml')}", - server: The url of the server to connect to
)
parser.add_argument("--key", "-k", default=None)
parser.add_argument("--server", "-S")
args = parser.parse_args()
:param args: The arguments from the command line
:type args: Namespace
:rtype: None
"""
try: try:
with open(args.config_file, encoding="utf8") as file: with open(args.config_file, encoding="utf8") as file:
config = load(file, Loader=Loader) config = load(file, Loader=Loader)
@ -450,7 +640,9 @@ def main() -> None:
if "config" not in config: if "config" not in config:
config["config"] = {} config["config"] = {}
config["config"] |= {"key": args.key} if "sources" not in config:
config["sources"] = {"youtube": {"enabled": True}}
if args.room: if args.room:
config["config"] |= {"room": args.room} config["config"] |= {"room": args.room}
if args.secret: if args.secret:
@ -459,7 +651,3 @@ def main() -> None:
config["config"] |= {"server": args.server} config["config"] |= {"server": args.server}
create_async_and_start_client(config) create_async_and_start_client(config)
if __name__ == "__main__":
main()

49
syng/config.py Normal file
View file

@ -0,0 +1,49 @@
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
class Option(Generic[T]):
pass
@dataclass
class ConfigOption(Generic[T]):
type: Option[T]
description: str
default: T
send_to_server: bool = False
class BoolOption(Option[bool]):
pass
class IntOption(Option[int]):
pass
class StrOption(Option[str]):
pass
class PasswordOption(Option[str]):
pass
class FolderOption(Option[str]):
pass
class FileOption(Option[str]):
pass
class ListStrOption(Option[list[str]]):
pass
@dataclass
class ChoiceOption(Option[str]):
choices: list[str]

View file

@ -1,4 +1,5 @@
"""Module for the entry of the queue.""" """Module for the entry of the queue."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
@ -22,9 +23,9 @@ class Entry:
:param duration: The duration of the song in seconds. :param duration: The duration of the song in seconds.
:type duration: int :type duration: int
:param title: The title of the song. :param title: The title of the song.
:type title: str :type title: Optional[str]
:param artist: The name of the original artist. :param artist: The name of the original artist.
:type artist: str :type artist: Optional[str]
:param album: The name of the album or compilation, this particular :param album: The name of the album or compilation, this particular
version is from. version is from.
:type album: str :type album: str
@ -51,8 +52,8 @@ class Entry:
ident: str ident: str
source: str source: str
duration: int duration: int
title: str title: Optional[str]
artist: str artist: Optional[str]
album: str album: str
performer: str performer: str
failed: bool = False failed: bool = False
@ -60,6 +61,7 @@ class Entry:
uuid: UUID = field(default_factory=uuid4) uuid: UUID = field(default_factory=uuid4)
uid: Optional[str] = None uid: Optional[str] = None
started_at: Optional[float] = None started_at: Optional[float] = None
incomplete_data: bool = False
def update(self, **kwargs: Any) -> None: def update(self, **kwargs: Any) -> None:
""" """
@ -72,11 +74,19 @@ class Entry:
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
def shares_performer(self, other_performer: str) -> bool: def shares_performer(self, other_performer: str) -> bool:
"""
Check if this entry shares a performer with another entry.
:param other_performer: The performer to check against.
:type other_performer: str
:return: True if the performers intersect, False otherwise.
:rtype: bool
"""
def normalize(performers: str) -> set[str]: def normalize(performers: str) -> set[str]:
return set( return set(
filter( filter(
lambda x: len(x) > 0 lambda x: len(x) > 0 and x not in ["der", "die", "das", "alle", "und"],
and x not in ["der", "die", "das", "alle", "und"],
re.sub( re.sub(
r"[^a-zA-Z0-9\s]", r"[^a-zA-Z0-9\s]",
"", "",

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
"""Wraps the ``json`` module, so that own classes get encoded.""" """Wraps the ``json`` module, so that own classes get encoded."""
import json import json
from dataclasses import asdict from dataclasses import asdict
from typing import Any from typing import Any

3
syng/log.py Normal file
View file

@ -0,0 +1,3 @@
import logging
logger = logging.getLogger("Syng")

138
syng/main.py Normal file
View file

@ -0,0 +1,138 @@
"""
Main entry point for the application.
This module contains the main entry point for the application. It parses the
command line arguments and runs the appropriate function based on the arguments.
This module also checks if the client and server modules are available and
imports them if they are. If they are not available, the application will not
run the client or server functions.
Client usage: syng client [-h] [--room ROOM] [--secret SECRET] \
[--config-file CONFIG_FILE] [--server SERVER]
Server usage: syng server [-h] [--host HOST] [--port PORT] [--root-folder ROOT_FOLDER] \
[--registration-keyfile REGISTRATION_KEYFILE] [--private] [--restricted] \
[--admin-password PASSWORD]
GUI usage: syng gui
The config file for the client should be a yaml file in the following style::
sources:
SOURCE1:
configuration for SOURCE
SOURCE2:
configuration for SOURCE
...
config:
server: ...
room: ...
preview_duration: ...
secret: ...
last_song: ...
waiting_room_policy: ..
key: ..
"""
from typing import TYPE_CHECKING
from argparse import ArgumentParser
import os
import multiprocessing
import traceback
import platformdirs
gui_exception = ""
try:
from syng.gui import run_gui
GUI_AVAILABLE = True
except ImportError:
if TYPE_CHECKING:
from syng.gui import run_gui
gui_exception = traceback.format_exc()
GUI_AVAILABLE = False
try:
from .client import run_client
CLIENT_AVAILABLE = True
except ImportError:
if TYPE_CHECKING:
from .client import run_client
CLIENT_AVAILABLE = False
try:
from .server import run_server
SERVER_AVAILABLE = True
except ImportError:
if TYPE_CHECKING:
from .server import run_server
SERVER_AVAILABLE = False
def main() -> None:
"""
Main entry point for the application.
This function parses the command line arguments and runs the appropriate
function based on the arguments.
:return: None
"""
parser: ArgumentParser = ArgumentParser()
sub_parsers = parser.add_subparsers(dest="action")
if CLIENT_AVAILABLE:
client_parser = sub_parsers.add_parser("client")
client_parser.add_argument("--room", "-r")
client_parser.add_argument("--secret", "-s")
client_parser.add_argument(
"--config-file",
"-C",
default=f"{os.path.join(platformdirs.user_config_dir('syng'), 'config.yaml')}",
)
# client_parser.add_argument("--key", "-k", default=None)
client_parser.add_argument("--server", "-S")
if GUI_AVAILABLE:
sub_parsers.add_parser("gui")
if SERVER_AVAILABLE:
root_path = os.path.join(os.path.dirname(__file__), "static")
server_parser = sub_parsers.add_parser("server")
server_parser.add_argument("--host", "-H", default="localhost")
server_parser.add_argument("--port", "-p", type=int, default=8080)
server_parser.add_argument("--root-folder", "-r", default=root_path)
server_parser.add_argument("--registration-keyfile", "-k", default=None)
server_parser.add_argument("--private", "-P", action="store_true", default=False)
server_parser.add_argument("--restricted", "-R", action="store_true", default=False)
server_parser.add_argument("--admin-password", "-A", default=None)
args = parser.parse_args()
if args.action == "client":
run_client(args)
elif args.action == "server":
run_server(args)
elif args.action == "gui":
if not GUI_AVAILABLE:
print("GUI module is not available.")
print(gui_exception)
else:
run_gui()
else:
if not GUI_AVAILABLE:
print("GUI module is not available.")
print(gui_exception)
else:
run_gui()
if __name__ == "__main__":
if os.name == "nt":
multiprocessing.freeze_support()
main()

202
syng/player_libmpv.py Normal file
View file

@ -0,0 +1,202 @@
import asyncio
from enum import Enum
import locale
import sys
from typing import Callable, Iterable, Optional, cast
from qrcode.main import QRCode
import mpv
import os
from .entry import Entry
class QRPosition(Enum):
TOP_LEFT = 1
TOP_RIGHT = 2
BOTTOM_LEFT = 3
BOTTOM_RIGHT = 4
@staticmethod
def from_string(value: str) -> "QRPosition":
match value:
case "top-left":
return QRPosition.TOP_LEFT
case "top-right":
return QRPosition.TOP_RIGHT
case "bottom-left":
return QRPosition.BOTTOM_LEFT
case "bottom-right":
return QRPosition.BOTTOM_RIGHT
case _:
return QRPosition.BOTTOM_RIGHT
class Player:
def __init__(
self,
qr_string: str,
qr_box_size: int,
qr_position: QRPosition,
quit_callback: Callable[[], None],
) -> None:
locale.setlocale(locale.LC_ALL, "C")
self.base_dir = f"{os.path.dirname(__file__)}/static"
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
self.base_dir = getattr(sys, "_MEIPASS")
self.closing = False
self.mpv: Optional[mpv.MPV] = None
self.qr_overlay: Optional[mpv.ImageOverlay] = None
self.qr_box_size = qr_box_size
self.qr_position = qr_position
self.update_qr(
qr_string,
)
self.default_options = {
"scale": "bilinear",
}
self.quit_callback = quit_callback
self.callback_audio_load: Optional[str] = None
def start(self) -> None:
self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True)
self.mpv.title = "Syng - Player"
self.mpv.keep_open = "yes"
self.mpv.play(
f"{self.base_dir}/background.png",
)
self.mpv.observe_property("osd-width", self.osd_size_handler)
self.mpv.observe_property("osd-height", self.osd_size_handler)
self.mpv.register_event_callback(self.event_callback)
def event_callback(self, event: mpv.MpvEvent) -> None:
e = event.as_dict()
if e["event"] == b"shutdown":
if not self.closing:
self.closing = True
self.quit_callback()
elif e["event"] == b"file-loaded":
if self.callback_audio_load is not None and self.mpv is not None:
self.mpv.audio_add(self.callback_audio_load)
self.callback_audio_load = None
def update_qr(self, qr_string: str) -> None:
qr = QRCode(box_size=self.qr_box_size, border=1)
qr.add_data(qr_string)
qr.make()
self.qr = qr.make_image().convert("RGBA")
def osd_size_handler(self, attribute: str, value: int) -> None:
if self.mpv is None:
print("MPV is not initialized", file=sys.stderr)
return
if self.qr_overlay:
self.mpv.remove_overlay(self.qr_overlay.overlay_id)
osd_width: int = cast(int, self.mpv.osd_width)
osd_height: int = cast(int, self.mpv.osd_height)
match self.qr_position:
case QRPosition.BOTTOM_RIGHT:
x_pos = osd_width - self.qr.width - 10
y_pos = osd_height - self.qr.height - 10
case QRPosition.BOTTOM_LEFT:
x_pos = 10
y_pos = osd_height - self.qr.height - 10
case QRPosition.TOP_RIGHT:
x_pos = osd_width - self.qr.width - 10
y_pos = 10
case QRPosition.TOP_LEFT:
x_pos = 10
y_pos = 10
self.qr_overlay = self.mpv.create_image_overlay(self.qr, pos=(x_pos, y_pos))
async def queue_next(self, entry: Entry) -> None:
if self.mpv is None:
print("MPV is not initialized", file=sys.stderr)
return
loop = asyncio.get_running_loop()
frame = sys._getframe()
stream_name = f"__python_mpv_play_generator_{hash(frame)}"
@self.mpv.python_stream(stream_name)
def preview() -> Iterable[bytes]:
subtitle: str = f"""1
00:00:00,00 --> 00:05:00,00
{entry.artist} - {entry.title}
{entry.performer}"""
yield subtitle.encode()
preview.unregister()
self.mpv.sub_pos = 50
self.play_image(
f"{self.base_dir}/background20perc.png", 3, sub_file=f"python://{stream_name}"
)
try:
await loop.run_in_executor(None, self.mpv.wait_for_property, "eof-reached")
except mpv.ShutdownError:
self.quit_callback()
def play_image(self, image: str, duration: int, sub_file: Optional[str] = None) -> None:
if self.mpv is None:
print("MPV is not initialized", file=sys.stderr)
return
for property, value in self.default_options.items():
self.mpv[property] = value
self.mpv.image_display_duration = duration
self.mpv.keep_open = "yes"
if sub_file:
self.mpv.loadfile(image, sub_file=sub_file)
else:
self.mpv.loadfile(image)
self.mpv.pause = False
async def play(
self,
video: str,
audio: Optional[str] = None,
override_options: Optional[dict[str, str]] = None,
) -> None:
if self.mpv is None:
print("MPV is not initialized", file=sys.stderr)
return
if override_options is None:
override_options = {}
for property, value in self.default_options.items():
self.mpv[property] = value
for property, value in override_options.items():
self.mpv[property] = value
loop = asyncio.get_running_loop()
self.mpv.pause = True
if audio:
self.callback_audio_load = audio
self.mpv.loadfile(video)
else:
self.mpv.loadfile(video)
self.mpv.pause = False
try:
await loop.run_in_executor(None, self.mpv.wait_for_property, "eof-reached")
self.mpv.image_display_duration = 0
self.mpv.play(f"{self.base_dir}/background.png")
except mpv.ShutdownError:
self.quit_callback()
def skip_current(self) -> None:
if self.mpv is None:
print("MPV is not initialized", file=sys.stderr)
return
self.mpv.image_display_duration = 0
self.mpv.play(
f"{self.base_dir}/background.png",
)
# self.mpv.playlist_next()

View file

@ -1,4 +1,5 @@
"""A async queue with synchronization.""" """A async queue with synchronization."""
import asyncio import asyncio
from collections import deque from collections import deque
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
@ -106,6 +107,14 @@ class Queue:
updater(item) updater(item)
def find_by_name(self, name: str) -> Optional[Entry]: def find_by_name(self, name: str) -> Optional[Entry]:
"""
Find an entry by its performer and return it.
:param name: The name of the performer to search for.
:type name: str
:returns: The entry with the performer or `None` if no such entry exists
:rtype: Optional[Entry]
"""
for item in self._queue: for item in self._queue:
if item.shares_performer(name): if item.shares_performer(name):
return item return item
@ -172,3 +181,28 @@ class Queue:
tmp = self._queue[uuid_idx] tmp = self._queue[uuid_idx]
self._queue[uuid_idx] = self._queue[uuid_idx - 1] self._queue[uuid_idx] = self._queue[uuid_idx - 1]
self._queue[uuid_idx - 1] = tmp self._queue[uuid_idx - 1] = tmp
async def move_to(self, uuid: str, target: int) -> None:
"""
Move an :py:class:`syng.entry.Entry` with the uuid to a specific position.
:param uuid: The uuid of the entry.
:type uuid: str
:param target: The target position.
:type target: int
:rtype: None
"""
async with self.readlock:
uuid_idx = 0
for idx, item in enumerate(self._queue):
if item.uuid == uuid or str(item.uuid) == uuid:
uuid_idx = idx
if uuid_idx != target:
entry = self._queue[uuid_idx]
self._queue.remove(entry)
if target > uuid_idx:
target = target - 1
self._queue.insert(target, entry)

2351
syng/resources.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
"""Module for search results.""" """Module for search results."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
import os.path import os.path
from typing import Optional
@dataclass @dataclass
@ -25,22 +26,21 @@ class Result:
ident: str ident: str
source: str source: str
title: str title: str
artist: str artist: Optional[str]
album: str album: Optional[str]
duration: Optional[str] = None
@staticmethod @classmethod
def from_filename(filename: str, source: str) -> Optional[Result]: def from_filename(cls, filename: str, source: str) -> Result:
""" """
Infere most attributes from the filename. Infer most attributes from the filename.
The filename must be in this form:: The filename must be in this form::
{artist} - {title} - {album}.cdg {artist} - {title} - {album}.ext
Although the extension (cdg) is not required If parsing failes, the filename will be used as the title and the
artist and album will be set to "Unknown".
If parsing failes, ``None`` is returned. Otherwise a Result object with
those attributes is created.
:param filename: The filename to parse :param filename: The filename to parse
:type filename: str :type filename: str
@ -49,12 +49,68 @@ class Result:
:return: see above :return: see above
:rtype: Optional[Result] :rtype: Optional[Result]
""" """
basename = os.path.splitext(filename)[0]
try: try:
splitfile = os.path.basename(filename[:-4]).split(" - ") splitfile = os.path.basename(basename).split(" - ")
ident = filename ident = filename
artist = splitfile[0].strip() artist = splitfile[0].strip()
title = splitfile[1].strip() title = splitfile[1].strip()
album = splitfile[2].strip() album = splitfile[2].strip()
return Result(ident, source, title, artist, album) return cls(ident=ident, source=source, title=title, artist=artist, album=album)
except IndexError: except IndexError:
return None return cls(ident=filename, source=source, title=basename, artist=None, album=None)
@classmethod
def from_dict(cls, values: dict[str, str]) -> Result:
"""
Create a Result object from a dictionary.
The dictionary must have the following keys:
- ident (str)
- source (str)
- title (str)
- artist (str)
- album (str)
- duration (int, optional)
:param values: The dictionary with the values
:type values: dict[str, str]
:return: The Result object
:rtype: Result
"""
return cls(
ident=values["ident"],
source=values["source"],
title=values["title"],
artist=values["artist"],
album=values["album"],
duration=values.get("duration", None),
)
def to_dict(self) -> dict[str, str]:
"""
Convert the Result object to a dictionary.
The dictionary will have the following keys:
- ident (str)
- source (str)
- title (str)
- album (str, if available)
- artist (str, if available)
- duration (str, if available)
:return: The dictionary with the values
:rtype: dict[str, str]
"""
output: dict[str, str] = {
"ident": self.ident,
"source": self.source,
"title": self.title,
}
if self.album is not None:
output["album"] = self.album
if self.artist is not None:
output["artist"] = self.artist
if self.duration is not None:
output["duration"] = self.duration
return output

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
Imports all sources, so that they add themselves to the Imports all sources, so that they add themselves to the
``available_sources`` dictionary. ``available_sources`` dictionary.
""" """
# pylint: disable=useless-import-alias # pylint: disable=useless-import-alias
from typing import Any from typing import Any

View file

@ -1,51 +1,75 @@
"""Module for an abstract filebased Source.""" """Module for an abstract filebased Source."""
import asyncio import asyncio
import os import os
from typing import Any, Optional from typing import TYPE_CHECKING, Any, Optional
from syng.entry import Entry
try: try:
from pymediainfo import MediaInfo from pymediainfo import MediaInfo
PYMEDIAINFO_AVAILABLE = True PYMEDIAINFO_AVAILABLE = True
except ImportError: except ImportError:
if TYPE_CHECKING:
from pymediainfo import MediaInfo
PYMEDIAINFO_AVAILABLE = False PYMEDIAINFO_AVAILABLE = False
from .source import Source from .source import Source
from ..config import ListStrOption, ConfigOption
class FileBasedSource(Source): class FileBasedSource(Source):
"""A source for indexing and playing songs from a local folder. """
A abstract source for indexing and playing songs based on files.
Config options are: Config options are:
-``dir``, dirctory to index and server from. -``extensions``, list of filename extensions
""" """
config_schema = Source.config_schema | { config_schema = Source.config_schema | {
"extensions": ( "extensions": ConfigOption(
list, ListStrOption(),
"List of filename extensions\n(mp3+cdg, mp4, ...)", "List of filename extensions\n(mp3+cdg, mp4, ...)",
["mp3+cdg"], ["mp3+cdg"],
) ),
} }
def __init__(self, config: dict[str, Any]): def apply_config(self, config: dict[str, Any]) -> None:
"""Initialize the file module.""" self.build_index = True
super().__init__(config)
self.extensions: list[str] = config["extensions"] if "extensions" in config else ["mp3+cdg"] self.extensions: list[str] = config["extensions"] if "extensions" in config else ["mp3+cdg"]
self.extra_mpv_arguments = ["--scale=oversample"] self.extra_mpv_options = {"scale": "oversample"}
def has_correct_extension(self, path: str) -> bool: def is_valid(self, entry: Entry) -> bool:
"""Check if a `path` has a correct extension. return entry.ident in self._index and entry.source == self.source_name
def has_correct_extension(self, path: Optional[str]) -> bool:
"""
Check if a `path` has a correct extension.
For A+B type extensions (like mp3+cdg) only the latter halve is checked For A+B type extensions (like mp3+cdg) only the latter halve is checked
:param path: The path to check.
:type path: Optional[str]
:return: True iff path has correct extension. :return: True iff path has correct extension.
:rtype: bool :rtype: bool
""" """
return os.path.splitext(path)[1][1:] in [ext.split("+")[-1] for ext in self.extensions] return path is not None and os.path.splitext(path)[1][1:] in [
ext.rsplit("+", maxsplit=1)[-1] for ext in self.extensions
]
def get_video_audio_split(self, path: str) -> tuple[str, Optional[str]]: def get_video_audio_split(self, path: str) -> tuple[str, Optional[str]]:
"""
Returns path for audio and video file, if filetype is marked as split.
If the file is not marked as split, the second element of the tuple will be None.
:params: path: The path to the file
:type path: str
:return: Tuple with path to video and audio file
:rtype: tuple[str, Optional[str]]
"""
extension_of_path = os.path.splitext(path)[1][1:] extension_of_path = os.path.splitext(path)[1][1:]
splitted_extensions = [ext.split("+") for ext in self.extensions if "+" in ext] splitted_extensions = [ext.split("+") for ext in self.extensions if "+" in ext]
splitted_extensions_dict = {video: audio for [audio, video] in splitted_extensions} splitted_extensions_dict = {video: audio for [audio, video] in splitted_extensions}
@ -58,6 +82,14 @@ class FileBasedSource(Source):
return (path, None) return (path, None)
async def get_duration(self, path: str) -> int: async def get_duration(self, path: str) -> int:
"""
Return the duration for the file.
:param path: The path to the file
:type path: str
:return: The duration in seconds
:rtype: int
"""
if not PYMEDIAINFO_AVAILABLE: if not PYMEDIAINFO_AVAILABLE:
return 180 return 180

View file

@ -1,12 +1,15 @@
"""Module for the files Source.""" """Module for the files Source."""
import asyncio import asyncio
import os import os
from typing import Any, Optional from typing import Any, Optional
from typing import Tuple from typing import Tuple
from ..entry import Entry from ..entry import Entry
from .source import available_sources from .source import available_sources
from .filebased import FileBasedSource from .filebased import FileBasedSource
from ..config import FolderOption, ConfigOption
class FilesSource(FileBasedSource): class FilesSource(FileBasedSource):
@ -18,16 +21,13 @@ class FilesSource(FileBasedSource):
source_name = "files" source_name = "files"
config_schema = FileBasedSource.config_schema | { config_schema = FileBasedSource.config_schema | {
"dir": (str, "Directory to index", "."), "dir": ConfigOption(FolderOption(), "Directory to index", "."),
"index_file": (str, "Index file", "files-index"), # "index_file": ("file", "Index file", os.path.join(user_cache_dir("syng"), "files-index")),
} }
def __init__(self, config: dict[str, Any]): def apply_config(self, config: dict[str, Any]) -> None:
"""Initialize the file module.""" super().apply_config(config)
super().__init__(config)
self.dir = config["dir"] if "dir" in config else "." self.dir = config["dir"] if "dir" in config else "."
self.extra_mpv_arguments = ["--scale=oversample"]
async def get_file_list(self) -> list[str]: async def get_file_list(self) -> list[str]:
"""Collect all files in ``dir``, that have the correct filename extension""" """Collect all files in ``dir``, that have the correct filename extension"""
@ -57,7 +57,7 @@ class FilesSource(FileBasedSource):
return {"duration": duration} return {"duration": duration}
async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]: async def do_buffer(self, entry: Entry, pos: int) -> Tuple[str, Optional[str]]:
""" """
No buffering needs to be done, since the files are already on disk. No buffering needs to be done, since the files are already on disk.

View file

@ -3,21 +3,28 @@ Construct the S3 source.
Adds it to the ``available_sources`` with the name ``s3`` Adds it to the ``available_sources`` with the name ``s3``
""" """
import asyncio import asyncio
import os import os
from json import dump, load from json import dump, load
from typing import Any, Optional, Tuple, cast from typing import TYPE_CHECKING, Any, Optional, Tuple, cast
from platformdirs import user_cache_dir
try: try:
from minio import Minio from minio import Minio
MINIO_AVAILABE = True MINIO_AVAILABE = True
except ImportError: except ImportError:
if TYPE_CHECKING:
from minio import Minio
MINIO_AVAILABE = False MINIO_AVAILABE = False
from ..entry import Entry from ..entry import Entry
from .filebased import FileBasedSource from .filebased import FileBasedSource
from .source import available_sources from .source import available_sources
from ..config import BoolOption, ConfigOption, FileOption, FolderOption, PasswordOption, StrOption
class S3Source(FileBasedSource): class S3Source(FileBasedSource):
@ -27,7 +34,7 @@ class S3Source(FileBasedSource):
- ``endpoint``, ``access_key``, ``secret_key``, ``secure``, ``bucket``: These - ``endpoint``, ``access_key``, ``secret_key``, ``secure``, ``bucket``: These
will simply be forwarded to the ``minio`` client. will simply be forwarded to the ``minio`` client.
- ``tmp_dir``: The folder, where temporary files are stored. Default - ``tmp_dir``: The folder, where temporary files are stored. Default
is ``/tmp/syng`` is ``${XDG_CACHE_DIR}/syng``
- ``index_file``: If the file does not exist, saves the paths of - ``index_file``: If the file does not exist, saves the paths of
files from the s3 instance to this file. If it exists, loads files from the s3 instance to this file. If it exists, loads
the list of files from this file. the list of files from this file.
@ -35,19 +42,23 @@ class S3Source(FileBasedSource):
source_name = "s3" source_name = "s3"
config_schema = FileBasedSource.config_schema | { config_schema = FileBasedSource.config_schema | {
"endpoint": (str, "Endpoint of the s3", ""), "endpoint": ConfigOption(StrOption(), "Endpoint of the s3", ""),
"access_key": (str, "Access Key of the s3", ""), "access_key": ConfigOption(StrOption(), "Access Key of the s3 (username)", ""),
"secret_key": (str, "Secret Key of the s3", ""), "secret_key": ConfigOption(PasswordOption(), "Secret Key of the s3 (password)", ""),
"secure": (bool, "Use SSL", True), "secure": ConfigOption(BoolOption(), "Use SSL", True),
"bucket": (str, "Bucket of the s3", ""), "bucket": ConfigOption(StrOption(), "Bucket of the s3", ""),
"tmp_dir": (str, "Folder for\ntemporary download", "/tmp/syng"), "tmp_dir": ConfigOption(
"index_file": (str, "Index file", "s3-index"), FolderOption(), "Folder for\ntemporary download", user_cache_dir("syng")
),
"index_file": ConfigOption(
FileOption(),
"Index file",
os.path.join(user_cache_dir("syng"), "s3-index"),
),
} }
def __init__(self, config: dict[str, Any]): def apply_config(self, config: dict[str, Any]) -> None:
"""Create the source.""" super().apply_config(config)
super().__init__(config)
if ( if (
MINIO_AVAILABE MINIO_AVAILABE
and "endpoint" in config and "endpoint" in config
@ -64,7 +75,32 @@ class S3Source(FileBasedSource):
self.tmp_dir: str = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng" self.tmp_dir: str = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng"
self.index_file: Optional[str] = config["index_file"] if "index_file" in config else None self.index_file: Optional[str] = config["index_file"] if "index_file" in config else None
self.extra_mpv_arguments = ["--scale=oversample"]
def load_file_list_from_server(self) -> list[str]:
"""
Load the file list from the s3 instance.
:return: A list of file paths
:rtype: list[str]
"""
file_list = [
obj.object_name
for obj in self.minio.list_objects(self.bucket, recursive=True)
if obj.object_name is not None and self.has_correct_extension(obj.object_name)
]
return file_list
def write_index(self, file_list: list[str]) -> None:
if self.index_file is None:
return
index_dir = os.path.dirname(self.index_file)
if index_dir:
os.makedirs(os.path.dirname(self.index_file), exist_ok=True)
with open(self.index_file, "w", encoding="utf8") as index_file_handle:
dump(file_list, index_file_handle)
async def get_file_list(self) -> list[str]: async def get_file_list(self) -> list[str]:
""" """
@ -83,18 +119,29 @@ class S3Source(FileBasedSource):
with open(self.index_file, "r", encoding="utf8") as index_file_handle: with open(self.index_file, "r", encoding="utf8") as index_file_handle:
return cast(list[str], load(index_file_handle)) return cast(list[str], load(index_file_handle))
file_list = [ file_list = self.load_file_list_from_server()
obj.object_name
for obj in self.minio.list_objects(self.bucket, recursive=True)
if self.has_correct_extension(obj.object_name)
]
if self.index_file is not None and not os.path.isfile(self.index_file): if self.index_file is not None and not os.path.isfile(self.index_file):
with open(self.index_file, "w", encoding="utf8") as index_file_handle: self.write_index(file_list)
dump(file_list, index_file_handle)
return file_list return file_list
return await asyncio.to_thread(_get_file_list) return await asyncio.to_thread(_get_file_list)
async def update_file_list(self) -> Optional[list[str]]:
"""
Rescan the file list and update the index file.
:return: The updated file list
:rtype: list[str]
"""
def _update_file_list() -> list[str]:
file_list = self.load_file_list_from_server()
self.write_index(file_list)
return file_list
return await asyncio.to_thread(_update_file_list)
async def get_missing_metadata(self, entry: Entry) -> dict[str, Any]: async def get_missing_metadata(self, entry: Entry) -> dict[str, Any]:
""" """
Return the duration for the music file. Return the duration for the music file.
@ -114,7 +161,7 @@ class S3Source(FileBasedSource):
return {"duration": duration} return {"duration": duration}
async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]: async def do_buffer(self, entry: Entry, pos: int) -> Tuple[str, Optional[str]]:
""" """
Download the file from the s3. Download the file from the s3.
@ -135,8 +182,9 @@ class S3Source(FileBasedSource):
asyncio.to_thread(self.minio.fget_object, self.bucket, entry.ident, video_dl_path) asyncio.to_thread(self.minio.fget_object, self.bucket, entry.ident, video_dl_path)
) )
audio_dl_path: Optional[str]
if audio_path is not None: if audio_path is not None:
audio_dl_path: Optional[str] = os.path.join(self.tmp_dir, audio_path) audio_dl_path = os.path.join(self.tmp_dir, audio_path)
audio_dl_task: asyncio.Task[Any] = asyncio.create_task( audio_dl_task: asyncio.Task[Any] = asyncio.create_task(
asyncio.to_thread(self.minio.fget_object, self.bucket, audio_path, audio_dl_path) asyncio.to_thread(self.minio.fget_object, self.bucket, audio_path, audio_dl_path)

View file

@ -4,10 +4,10 @@ Abstract class for sources.
Also defines the dictionary of available sources. Each source should add itself Also defines the dictionary of available sources. Each source should add itself
to this dictionary in its module. to this dictionary in its module.
""" """
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging
import os.path import os.path
import shlex import shlex
from collections import defaultdict from collections import defaultdict
@ -21,10 +21,15 @@ from typing import Tuple
from typing import Type from typing import Type
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from ..log import logger
from ..entry import Entry from ..entry import Entry
from ..result import Result from ..result import Result
from ..config import BoolOption, ConfigOption
logger: logging.Logger = logging.getLogger(__name__)
class EntryNotValid(Exception):
"""Raised when an entry is not valid for a source."""
@dataclass @dataclass
@ -45,9 +50,6 @@ class DLFilesEntry:
:param complete: True if download was completed, False otherwise (Default :param complete: True if download was completed, False otherwise (Default
is ``False``) is ``False``)
:type complete: bool :type complete: bool
:param failed: True if the buffering failed, False otherwise (Default is
``False``)
:type failed: bool
:param skip: True if the next Entry for this file should be skipped :param skip: True if the next Entry for this file should be skipped
(Default is ``False``) (Default is ``False``)
:param buffer_task: Reference to the task, that downloads the files. :param buffer_task: Reference to the task, that downloads the files.
@ -61,7 +63,6 @@ class DLFilesEntry:
audio: Optional[str] = None audio: Optional[str] = None
buffering: bool = False buffering: bool = False
complete: bool = False complete: bool = False
failed: bool = False
skip: bool = False skip: bool = False
buffer_task: Optional[asyncio.Task[Tuple[str, Optional[str]]]] = None buffer_task: Optional[asyncio.Task[Tuple[str, Optional[str]]]] = None
@ -75,7 +76,6 @@ class Source(ABC):
attribute. attribute.
Source specific tasks will be forwarded to the respective source, like: Source specific tasks will be forwarded to the respective source, like:
- Playing the audio/video
- Buffering the audio/video - Buffering the audio/video
- Searching for a query - Searching for a query
- Getting an entry from an identifier - Getting an entry from an identifier
@ -88,7 +88,7 @@ class Source(ABC):
``get_entry``, ``search``, ``add_to_config`` ``get_entry``, ``search``, ``add_to_config``
Specific client methods: Specific client methods:
``buffer``, ``do_buffer``, ``play``, ``skip_current``, ``ensure_playable``, ``buffer``, ``do_buffer``, ``skip_current``, ``ensure_playable``,
``get_missing_metadata``, ``get_config`` ``get_missing_metadata``, ``get_config``
Each source has a reference to all files, that are currently queued to Each source has a reference to all files, that are currently queued to
@ -99,14 +99,14 @@ class Source(ABC):
:py:attr:`Entry.ident` to :py:class:`DLFilesEntry`. :py:attr:`Entry.ident` to :py:class:`DLFilesEntry`.
- ``player``, the reference to the ``mpv`` process, if it has - ``player``, the reference to the ``mpv`` process, if it has
started started
- ``extra_mpv_arguments``, list of arguments added to the mpv - ``extra_mpv_options``, dictionary of arguments added to the mpv
instance, can be overwritten by a subclass instance, can be overwritten by a subclass
- ``source_name``, the string used to identify the source - ``source_name``, the string used to identify the source
""" """
source_name: str = "" source_name: str = ""
config_schema: dict[str, tuple[type | list[type], str, Any]] = { config_schema: dict[str, ConfigOption[Any]] = {
"enabled": (bool, "Enable this source", False) "enabled": ConfigOption(BoolOption(), "Enable this source", False)
} }
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):
@ -120,41 +120,36 @@ class Source(ABC):
source for documentation. source for documentation.
:type config: dict[str, Any] :type config: dict[str, Any]
""" """
self.config: dict[str, Any] = config
self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry) self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry)
self._masterlock: asyncio.Lock = asyncio.Lock() self._masterlock: asyncio.Lock = asyncio.Lock()
self.player: Optional[asyncio.subprocess.Process] = None
self._index: list[str] = config["index"] if "index" in config else [] self._index: list[str] = config["index"] if "index" in config else []
self.extra_mpv_arguments: list[str] = [] self.extra_mpv_options: dict[str, str] = {}
self._skip_next = False self._skip_next = False
self.build_index = False
self.apply_config(config)
@staticmethod def is_valid(self, entry: Entry) -> bool:
async def play_mpv(
video: str, audio: Optional[str], /, *options: str
) -> asyncio.subprocess.Process:
""" """
Create a mpv process to play a song in full screen. Check if the entry is valid.
:param video: Location of the video part. Each source can implement this method to check if the entry is valid.
:type video: str
:param audio: Location of the audio part, if it exists. :param entry: The entry to check
:type audio: Optional[str] :type entry: Entry
:param options: Extra arguments forwarded to the mpv player :returns: True if the entry is valid, False otherwise.
:type options: str :rtype: bool
:returns: An async reference to the process
:rtype: asyncio.subprocess.Process
""" """
args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else []) return True
print(f"File is {video=} and {audio=}") async def get_entry(
self,
mpv_process = asyncio.create_subprocess_exec( performer: str,
"mpv", ident: str,
*args, /,
stdout=asyncio.subprocess.PIPE, artist: Optional[str] = None,
) title: Optional[str] = None,
return await mpv_process ) -> Optional[Entry]:
async def get_entry(self, performer: str, ident: str) -> Optional[Entry]:
""" """
Create an :py:class:`syng.entry.Entry` from a given identifier. Create an :py:class:`syng.entry.Entry` from a given identifier.
@ -173,22 +168,23 @@ class Source(ABC):
:returns: New entry for the identifier, or None, if the ident is :returns: New entry for the identifier, or None, if the ident is
invalid. invalid.
:rtype: Optional[Entry] :rtype: Optional[Entry]
:raises EntryNotValid: If the entry is not valid for the source.
""" """
if ident not in self._index:
return None
res: Optional[Result] = Result.from_filename(ident, self.source_name) res: Result = Result.from_filename(ident, self.source_name)
if res is not None: entry = Entry(
return Entry(
ident=ident, ident=ident,
source=self.source_name, source=self.source_name,
duration=180, duration=180,
album=res.album, album=res.album if res.album else "Unknown",
title=res.title, title=res.title if res.title else title if title else "Unknown",
artist=res.artist, artist=res.artist if res.artist else artist if artist else "Unknown",
performer=performer, performer=performer,
incomplete_data=True,
) )
return None if not self.is_valid(entry):
raise EntryNotValid(f"Entry {entry} is not valid for source {self.source_name}")
return entry
async def search(self, query: str) -> list[Result]: async def search(self, query: str) -> list[Result]:
""" """
@ -204,14 +200,11 @@ class Source(ABC):
filtered: list[str] = self.filter_data_by_query(query, self._index) filtered: list[str] = self.filter_data_by_query(query, self._index)
results: list[Result] = [] results: list[Result] = []
for filename in filtered: for filename in filtered:
result: Optional[Result] = Result.from_filename(filename, self.source_name) results.append(Result.from_filename(filename, self.source_name))
if result is None:
continue
results.append(result)
return results return results
@abstractmethod @abstractmethod
async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]: async def do_buffer(self, entry: Entry, pos: int) -> Tuple[str, Optional[str]]:
""" """
Source specific part of buffering. Source specific part of buffering.
@ -224,11 +217,13 @@ class Source(ABC):
:param entry: The entry to buffer :param entry: The entry to buffer
:type entry: Entry :type entry: Entry
:param pos: The position in the queue, the entry is at.
:type pos: int
:returns: A Tuple of the locations for the video and the audio file. :returns: A Tuple of the locations for the video and the audio file.
:rtype: Tuple[str, Optional[str]] :rtype: Tuple[str, Optional[str]]
""" """
async def buffer(self, entry: Entry) -> None: async def buffer(self, entry: Entry, pos: int) -> None:
""" """
Buffer all necessary files for the entry. Buffer all necessary files for the entry.
@ -242,6 +237,8 @@ class Source(ABC):
:param entry: The entry to buffer :param entry: The entry to buffer
:type entry: Entry :type entry: Entry
:param pos: The position in the queue, the entry is at.
:type pos: int
:rtype: None :rtype: None
""" """
async with self._masterlock: async with self._masterlock:
@ -250,54 +247,21 @@ class Source(ABC):
self.downloaded_files[entry.ident].buffering = True self.downloaded_files[entry.ident].buffering = True
try: try:
buffer_task = asyncio.create_task(self.do_buffer(entry)) buffer_task = asyncio.create_task(self.do_buffer(entry, pos))
self.downloaded_files[entry.ident].buffer_task = buffer_task self.downloaded_files[entry.ident].buffer_task = buffer_task
video, audio = await buffer_task video, audio = await buffer_task
self.downloaded_files[entry.ident].video = video self.downloaded_files[entry.ident].video = video
self.downloaded_files[entry.ident].audio = audio self.downloaded_files[entry.ident].audio = audio
self.downloaded_files[entry.ident].complete = True self.downloaded_files[entry.ident].complete = True
except ValueError as exc:
raise exc
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
print_exc() print_exc()
logger.error("Buffering failed for %s", entry) raise ValueError("Buffering failed for %s" % entry)
self.downloaded_files[entry.ident].failed = True
self.downloaded_files[entry.ident].ready.set() self.downloaded_files[entry.ident].ready.set()
async def play(self, entry: Entry) -> None:
"""
Play the entry.
This waits until buffering is complete and starts
playing the entry.
:param entry: The entry to play
:type entry: Entry
:rtype: None
"""
await self.ensure_playable(entry)
if self.downloaded_files[entry.ident].failed:
del self.downloaded_files[entry.ident]
return
async with self._masterlock:
if self._skip_next:
self._skip_next = False
entry.skip = True
return
self.player = await self.play_mpv(
self.downloaded_files[entry.ident].video,
self.downloaded_files[entry.ident].audio,
*self.extra_mpv_arguments,
)
await self.player.wait()
self.player = None
if self._skip_next:
self._skip_next = False
entry.skip = True
async def skip_current(self, entry: Entry) -> None: async def skip_current(self, entry: Entry) -> None:
""" """
Skips first song in the queue. Skips first song in the queue.
@ -318,10 +282,7 @@ class Source(ABC):
buffer_task.cancel() buffer_task.cancel()
self.downloaded_files[entry.ident].ready.set() self.downloaded_files[entry.ident].ready.set()
if self.player is not None: async def ensure_playable(self, entry: Entry) -> tuple[str, Optional[str]]:
self.player.kill()
async def ensure_playable(self, entry: Entry) -> None:
""" """
Guaranties that the given entry can be played. Guaranties that the given entry can be played.
@ -331,8 +292,10 @@ class Source(ABC):
:type entry: Entry :type entry: Entry
:rtype: None :rtype: None
""" """
await self.buffer(entry) await self.buffer(entry, 0)
await self.downloaded_files[entry.ident].ready.wait() dlfilesentry = self.downloaded_files[entry.ident]
await dlfilesentry.ready.wait()
return dlfilesentry.video, dlfilesentry.audio
async def get_missing_metadata(self, _entry: Entry) -> dict[str, Any]: async def get_missing_metadata(self, _entry: Entry) -> dict[str, Any]:
""" """
@ -384,6 +347,46 @@ class Source(ABC):
""" """
return [] return []
async def update_file_list(self) -> Optional[list[str]]:
"""
Update the internal list of files.
This is called after the client sends its initial file list to the
server to update the list of files since the last time an index file
was written.
It should return None, if the list is already up to date.
Otherwise it should return the new list of files.
:rtype: Optional[list[str]]
"""
return None
async def update_config(self) -> Optional[dict[str, Any] | list[dict[str, Any]]]:
"""
Update the config of the source.
This is called after the client sends its initial config to the server to
update the config. E.g. to update the list of files, that should be send to
the server.
It returns None, if the config is already up to date.
Otherwise returns the new config.
:rtype: Optional[dict[str, Any] | list[dict[str, Any]]
"""
if not self.build_index:
return None
logger.warning(f"{self.source_name}: updating index")
new_index = await self.update_file_list()
logger.warning(f"{self.source_name}: done")
if new_index is not None:
self._index = new_index
return await self.get_config()
return None
async def get_config(self) -> dict[str, Any] | list[dict[str, Any]]: async def get_config(self) -> dict[str, Any] | list[dict[str, Any]]:
""" """
Return the part of the config, that should be send to the server. Return the part of the config, that should be send to the server.
@ -402,15 +405,29 @@ class Source(ABC):
:return: The part of the config, that should be sended to the server. :return: The part of the config, that should be sended to the server.
:rtype: dict[str, Any] | list[dict[str, Any]] :rtype: dict[str, Any] | list[dict[str, Any]]
""" """
packages = []
if self.build_index:
if not self._index: if not self._index:
self._index = [] self._index = []
print(f"{self.source_name}: generating index") logger.warning(f"{self.source_name}: generating index")
self._index = await self.get_file_list() self._index = await self.get_file_list()
print(f"{self.source_name}: done") logger.warning(f"{self.source_name}: done")
chunked = zip_longest(*[iter(self._index)] * 1000, fillvalue="") chunked = zip_longest(*[iter(self._index)] * 1000, fillvalue="")
return [{"index": list(filter(lambda x: x != "", chunk))} for chunk in chunked] packages = [{"index": list(filter(lambda x: x != "", chunk))} for chunk in chunked]
first_package = {
key: value
for key, value in self.config.items()
if self.config_schema[key].send_to_server
}
if not packages:
packages = [first_package]
else:
packages[0] |= first_package
if len(packages) == 1:
return first_package
return packages
def add_to_config(self, config: dict[str, Any]) -> None: def add_to_config(self, config: dict[str, Any], running_number: int) -> None:
""" """
Add the config to the own config. Add the config to the own config.
@ -420,11 +437,30 @@ class Source(ABC):
In the default configuration, this just adds the index key of the In the default configuration, this just adds the index key of the
config to the index attribute of the source config to the index attribute of the source
If the running_number is 0, the index will be reset.
:param config: The part of the config to add. :param config: The part of the config to add.
:type config: dict[str, Any] :type config: dict[str, Any]
:param running_number: The running number of the config
:type running_number: int
:rtype: None
"""
if running_number == 0:
self._index = []
self._index += config["index"]
@abstractmethod
def apply_config(self, config: dict[str, Any]) -> None:
"""
Apply the a config to the source.
This should be implemented by each source individually.
:param config: The part of the config to apply.
:type config: dict[str, Any]
:rtype: None :rtype: None
""" """
self._index += config["index"] pass
available_sources: dict[str, Type[Source]] = {} available_sources: dict[str, Type[Source]] = {}

View file

@ -1,36 +1,167 @@
""" """
Construct the YouTube source. Construct the YouTube source.
If available, downloading will be performed via yt-dlp, if not, pytube will be This source uses yt-dlp to search and download videos from YouTube.
used.
Adds it to the ``available_sources`` with the name ``youtube``. Adds it to the ``available_sources`` with the name ``youtube``.
""" """
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import shlex import shlex
from functools import partial from functools import partial
from urllib.parse import urlencode
from typing import Any, Optional, Tuple from typing import Any, Optional, Tuple
try: from yt_dlp import YoutubeDL
from pytube import Channel, Search, YouTube, exceptions, innertube from yt_dlp.utils import DownloadError
from platformdirs import user_cache_dir
PYTUBE_AVAILABLE = True
except ImportError:
PYTUBE_AVAILABLE = False
try:
from yt_dlp import YoutubeDL
YT_DLP_AVAILABLE = True
except ImportError:
print("No yt-dlp")
YT_DLP_AVAILABLE = False
from ..entry import Entry from ..entry import Entry
from ..result import Result from ..result import Result
from .source import Source, available_sources from .source import Source, available_sources
from ..config import (
BoolOption,
ChoiceOption,
FolderOption,
ListStrOption,
ConfigOption,
StrOption,
IntOption,
)
class YouTube:
"""
A minimal compatibility layer for the YouTube object of pytube, implemented via yt-dlp
"""
def __init__(self, url: Optional[str] = None, info: Optional[dict[str, Any]] = None):
"""
Construct a YouTube object from a url.
If the url is already in the cache, the object is constructed from the
cache. Otherwise yt-dlp is used to extract the information.
:param url: The url of the video.
:type url: Optional[str]
"""
self._title: Optional[str]
self._author: Optional[str]
if url is not None:
try:
if info is not None:
self._infos = info
else:
self._infos = YoutubeDL({"quiet": True}).extract_info(url, download=False)
except DownloadError:
self.length = 300
self._title = None
self._author = None
self.watch_url = url
return
if self._infos is None:
raise RuntimeError(f'Extraction not possible for "{url}"')
self.length = int(self._infos["duration"])
self._title = self._infos["title"]
self._author = self._infos["channel"]
self.watch_url = url
else:
self.length = 0
self._title = ""
self.channel = ""
self._author = ""
self.watch_url = ""
@property
def title(self) -> str:
"""
The title of the video.
:return: The title of the video.
:rtype: str
"""
if self._title is None:
return ""
return self._title
@property
def author(self) -> str:
"""
The author of the video.
:return: The author of the video.
:rtype: str
"""
if self._author is None:
return ""
return self._author
@classmethod
def from_result(cls, search_result: dict[str, Any]) -> YouTube:
"""
Construct a YouTube object from yt-dlp search results.
:param search_result: The search result from yt-dlp.
:type search_result: dict[str, Any]
"""
url = search_result["url"]
return cls(url, info=search_result)
class Search:
"""
A minimal compatibility layer for the Search object of pytube, implemented via yt-dlp
"""
# pylint: disable=too-few-public-methods
def __init__(self, query: str, channel: Optional[str] = None):
"""
Construct a Search object from a query and an optional channel.
Uses yt-dlp to search for the query.
If no channel is given, the search is done on the whole of YouTube.
:param query: The query to search for.
:type query: str
:param channel: The channel to search in.
:type channel: Optional[str]
"""
sp = "EgIQAfABAQ==" # This is a magic string, that tells youtube to search for videos
if channel is None:
query_url = (
f"https://youtube.com/results?{urlencode({'search_query': query, 'sp': sp})}"
)
else:
if channel[0] == "/":
channel = channel[1:]
query_url = (
f"https://www.youtube.com/{channel}/search?{urlencode({'query': query, 'sp':sp})}"
)
results = YoutubeDL(
{
"extract_flat": True,
"quiet": True,
"playlist_items": ",".join(map(str, range(1, 51))),
}
).extract_info(
query_url,
download=False,
)
self.results = []
if results is not None:
filtered_entries = filter(lambda entry: "short" not in entry["url"], results["entries"])
for r in filtered_entries:
try:
self.results.append(YouTube.from_result(r))
except KeyError:
pass
class YoutubeSource(Source): class YoutubeSource(Source):
@ -41,44 +172,65 @@ class YoutubeSource(Source):
Examples are ``/c/CCKaraoke`` or Examples are ``/c/CCKaraoke`` or
``/channel/UCwTRjvjVge51X-ILJ4i22ew`` ``/channel/UCwTRjvjVge51X-ILJ4i22ew``
- ``tmp_dir``: The folder, where temporary files are stored. Default - ``tmp_dir``: The folder, where temporary files are stored. Default
is ``/tmp/syng`` is ``${XDG_CACHE_DIR}/syng``.
- ``max_res``: The highest video resolution, that should be - ``max_res``: The highest video resolution, that should be
downloaded/streamed. Default is 720. downloaded/streamed. Default is 720.
- ``start_streaming``: If set to ``True``, the client starts streaming - ``start_streaming``: If set to ``True``, the client starts streaming
the video, if buffering was not completed. Needs ``youtube-dl`` or the video, if buffering was not completed. Needs ``youtube-dl`` or
``yt-dlp``. Default is False. ``yt-dlp``. Default is False.
- ``search_suffix``: A string that is appended to the search query.
Default is "karaoke".
- ``max_duration``: The maximum duration of a video in seconds. A value of 0 disables this. Default is 1800.
""" """
source_name = "youtube" source_name = "youtube"
config_schema = Source.config_schema | { config_schema = Source.config_schema | {
"channels": (list, "A list channels\nto search in", []), "enabled": ConfigOption(BoolOption(), "Enable this source", True),
"tmp_dir": (str, "Folder for\ntemporary download", "/tmp/syng"), "channels": ConfigOption(
"max_res": (int, "Maximum resolution\nto download", 720), ListStrOption(), "A list channels\nto search in", [], send_to_server=True
"start_streaming": ( ),
bool, "tmp_dir": ConfigOption(
FolderOption(), "Folder for\ntemporary download", user_cache_dir("syng")
),
"max_res": ConfigOption(
ChoiceOption(["144", "240", "360", "480", "720", "1080", "2160"]),
"Maximum resolution\nto download",
"720",
),
"start_streaming": ConfigOption(
BoolOption(),
"Start streaming if\ndownload is not complete", "Start streaming if\ndownload is not complete",
False, False,
), ),
"search_suffix": ConfigOption(
StrOption(),
"A string that is appended\nto each search query",
"karaoke",
send_to_server=True,
),
"max_duration": ConfigOption(
IntOption(),
"The maximum duration\nof a video in seconds\nA value of 0 disables this",
1800,
send_to_server=True,
),
} }
# pylint: disable=too-many-instance-attributes def apply_config(self, config: dict[str, Any]) -> None:
def __init__(self, config: dict[str, Any]):
"""Create the source."""
super().__init__(config)
if PYTUBE_AVAILABLE:
self.innertube_client: innertube.InnerTube = innertube.InnerTube(client="WEB")
self.channels: list[str] = config["channels"] if "channels" in config else [] self.channels: list[str] = config["channels"] if "channels" in config else []
self.tmp_dir: str = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng" self.tmp_dir: str = config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng"
self.max_res: int = config["max_res"] if "max_res" in config else 720 try:
self.max_res: int = int(config["max_res"])
except (ValueError, KeyError):
self.max_res = 720
self.start_streaming: bool = ( self.start_streaming: bool = (
config["start_streaming"] if "start_streaming" in config else False config["start_streaming"] if "start_streaming" in config else False
) )
self.formatstring = ( self.formatstring = (
f"bestvideo[height<={self.max_res}]+" f"bestaudio/best[height<={self.max_res}]" f"bestvideo[height<={self.max_res}]+" f"bestaudio/best[height<={self.max_res}]"
) )
if YT_DLP_AVAILABLE: self.search_suffix = config.get("search_suffix", "karaoke")
self.extra_mpv_options = {"ytdl-format": self.formatstring}
self._yt_dlp = YoutubeDL( self._yt_dlp = YoutubeDL(
params={ params={
"paths": {"home": self.tmp_dir}, "paths": {"home": self.tmp_dir},
@ -86,49 +238,47 @@ class YoutubeSource(Source):
"quiet": True, "quiet": True,
} }
) )
self.max_duration: int = config.get("max_duration", 1800)
async def get_config(self) -> dict[str, Any] | list[dict[str, Any]]: async def ensure_playable(self, entry: Entry) -> tuple[str, Optional[str]]:
""" """
Return the list of channels in a dictionary with key ``channels``. Ensure that the entry is playable.
:return: see above If the entry is not yet downloaded, download it.
:rtype: dict[str, Any]] If start_streaming is set, start streaming immediatly.
"""
return {"channels": self.channels}
async def play(self, entry: Entry) -> None: :param entry: The entry to download.
"""
Play the given entry.
If ``start_streaming`` is set and buffering is not yet done, starts
immediatly and forwards the url to ``mpv``.
Otherwise wait for buffering and start playing.
:param entry: The entry to play.
:type entry: Entry :type entry: Entry
:rtype: None :rtype: None
""" """
if self.start_streaming and not self.downloaded_files[entry.ident].complete:
self.player = await self.play_mpv(
entry.ident,
None,
"--script-opts=ytdl_hook-ytdl_path=yt-dlp," "ytdl_hook-exclude='%.pls$'",
f"--ytdl-format={self.formatstring}",
"--fullscreen",
)
await self.player.wait()
else:
await super().play(entry)
async def get_entry(self, performer: str, ident: str) -> Optional[Entry]: if entry.incomplete_data:
meta_info = await self.get_missing_metadata(entry)
entry.update(**meta_info)
if self.max_duration > 0 and entry.duration > self.max_duration:
raise ValueError(f"Video {entry.ident} too long.")
if self.start_streaming and not self.downloaded_files[entry.ident].complete:
return (entry.ident, None)
return await super().ensure_playable(entry)
async def get_entry(
self,
performer: str,
ident: str,
/,
artist: Optional[str] = None,
title: Optional[str] = None,
) -> Optional[Entry]:
""" """
Create an :py:class:`syng.entry.Entry` for the identifier. Create an :py:class:`syng.entry.Entry` for the identifier.
The identifier should be a youtube url. An entry is created with The identifier should be a youtube url. An entry is created with
all available metadata for the video. all available metadata for the video.
:param performer: The persong singing. :param performer: The person singing.
:type performer: str :type performer: str
:param ident: A url to a YouTube video. :param ident: A url to a YouTube video.
:type ident: str :type ident: str
@ -136,37 +286,24 @@ class YoutubeSource(Source):
:rtype: Optional[Entry] :rtype: Optional[Entry]
""" """
def _get_entry(performer: str, url: str) -> Optional[Entry]:
if not PYTUBE_AVAILABLE:
return None
try:
yt_song = YouTube(url)
try:
length = yt_song.length
except TypeError:
length = 180
return Entry( return Entry(
ident=url, ident=ident,
source="youtube", source="youtube",
duration=180,
album="YouTube", album="YouTube",
duration=length, title=title,
title=yt_song.title, artist=artist,
artist=yt_song.author,
performer=performer, performer=performer,
incomplete_data=True,
) )
except exceptions.PytubeError:
return None
return await asyncio.to_thread(_get_entry, performer, ident)
async def search(self, query: str) -> list[Result]: async def search(self, query: str) -> list[Result]:
""" """
Search YouTube and the configured channels for the query. Search YouTube and the configured channels for the query.
The first results are the results of the configured channels. The next The first results are the results of the configured channels. The next
results are the results from youtube as a whole, but the term "Karaoke" results are the results from youtube as a whole, a configurable suffix
is appended to the search query. is appended to the search query (default is "karaoke").
All results are sorted by how good they match to the search query, All results are sorted by how good they match to the search query,
respecting their original source (channel or YouTube as a whole). respecting their original source (channel or YouTube as a whole).
@ -180,6 +317,17 @@ class YoutubeSource(Source):
""" """
def _contains_index(query: str, result: YouTube) -> float: def _contains_index(query: str, result: YouTube) -> float:
"""
Calculate a score for the result.
The score is the ratio of how many words of the query are in the
title and author of the result.
:param query: The query to search for.
:type query: str
:param result: The result to score.
:type result: YouTube
"""
compare_string: str = result.title.lower() + " " + result.author.lower() compare_string: str = result.title.lower() + " " + result.author.lower()
hits: int = 0 hits: int = 0
queries: list[str] = shlex.split(query.lower()) queries: list[str] = shlex.split(query.lower())
@ -205,70 +353,56 @@ class YoutubeSource(Source):
title=result.title, title=result.title,
artist=result.author, artist=result.author,
album="YouTube", album="YouTube",
duration=str(result.length),
) )
for result in results for result in results
if self.max_duration == 0 or result.length <= self.max_duration
] ]
def is_valid(self, entry: Entry) -> bool:
"""
Check if the entry is valid.
An entry is valid, if the video is not too long.
:param entry: The entry to check.
:type entry: Entry
:return: True if the entry is valid, False otherwise.
:rtype: bool
"""
return self.max_duration == 0 or entry.duration <= self.max_duration
def _yt_search(self, query: str) -> list[YouTube]: def _yt_search(self, query: str) -> list[YouTube]:
"""Search youtube as a whole. """Search youtube as a whole.
Adds "karaoke" to the query. Adds a configurable suffix to the query. Default is "karaoke".
""" """
results: Optional[list[YouTube]] = Search(f"{query} karaoke").results suffix = f" {self.search_suffix}" if self.search_suffix else ""
if results is not None: return Search(f"{query}{suffix}").results
return results
return []
# pylint: disable=protected-access
def _channel_search(self, query: str, channel: str) -> list[YouTube]: def _channel_search(self, query: str, channel: str) -> list[YouTube]:
""" """
Search a channel for a query. Search a channel for a query.
A lot of black Magic happens here. A lot of black Magic happens here.
""" """
browse_id: str = Channel(f"https://www.youtube.com{channel}").channel_id return Search(f"{query} karaoke", channel).results
endpoint: str = f"{self.innertube_client.base_url}/browse"
data: dict[str, str] = { async def get_missing_metadata(self, entry: Entry) -> dict[str, Any]:
"query": query, """
"browseId": browse_id, Video metadata should be read on the client to avoid banning
"params": "EgZzZWFyY2g%3D", the server.
"""
if entry.incomplete_data or None in (entry.artist, entry.title):
youtube_video: YouTube = await asyncio.to_thread(YouTube, entry.ident)
return {
"duration": youtube_video.length,
"artist": youtube_video.author,
"title": youtube_video.title,
} }
data.update(self.innertube_client.base_data) return {}
results: dict[str, Any] = self.innertube_client._call_api(
endpoint, self.innertube_client.base_params, data
)
items: list[dict[str, Any]] = results["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][
-1
]["expandableTabRenderer"]["content"]["sectionListRenderer"]["contents"]
list_of_videos: list[YouTube] = [] async def do_buffer(self, entry: Entry, pos: int) -> Tuple[str, Optional[str]]:
for item in items:
try:
if (
"itemSectionRenderer" in item
and "videoRenderer" in item["itemSectionRenderer"]["contents"][0]
):
yt_url: str = (
"https://youtube.com/watch?v="
+ item["itemSectionRenderer"]["contents"][0]["videoRenderer"]["videoId"]
)
author: str = item["itemSectionRenderer"]["contents"][0]["videoRenderer"][
"ownerText"
]["runs"][0]["text"]
title: str = item["itemSectionRenderer"]["contents"][0]["videoRenderer"][
"title"
]["runs"][0]["text"]
yt_song: YouTube = YouTube(yt_url)
yt_song.author = author
yt_song.title = title
list_of_videos.append(yt_song)
except KeyError:
pass
return list_of_videos
async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]:
""" """
Download the video. Download the video.
@ -279,12 +413,26 @@ class YoutubeSource(Source):
location exists, the return value for the audio part will always be location exists, the return value for the audio part will always be
``None``. ``None``.
If pos is 0 and start_streaming is set, no buffering is done, instead the
youtube url is returned.
:param entry: The entry to download. :param entry: The entry to download.
:type entry: Entry :type entry: Entry
:param pos: The position in the video to start buffering.
:type pos: int
:return: The location of the video file and ``None``. :return: The location of the video file and ``None``.
:rtype: Tuple[str, Optional[str]] :rtype: Tuple[str, Optional[str]]
""" """
info = await asyncio.to_thread(self._yt_dlp.extract_info, entry.ident)
if self.max_duration > 0 and entry.duration > self.max_duration:
raise ValueError(
f"Video {entry.ident} too long: {entry.duration} > {self.max_duration}"
)
if pos == 0 and self.start_streaming:
return entry.ident, None
info: Any = await asyncio.to_thread(self._yt_dlp.extract_info, entry.ident)
combined_path = info["requested_downloads"][0]["filepath"] combined_path = info["requested_downloads"][0]["filepath"]
return combined_path, None return combined_path, None

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
syng/static/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng Rocks!</title> <title>Syng Rocks!</title>
<script type="module" crossorigin src="/assets/index.20e81f9f.js"></script> <script type="module" crossorigin src="/assets/index.520c2769.js"></script>
<link rel="stylesheet" href="/assets/index.b030f504.css"> <link rel="stylesheet" href="/assets/index.ed7016c8.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 11 KiB

105
syng/static/syng.svg Normal file
View file

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128"
height="128"
viewBox="0 0 33.866666 33.866667"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="rocks.syng.gui2.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showguides="true"
inkscape:zoom="4.6965769"
inkscape:cx="68.241191"
inkscape:cy="55.146548"
inkscape:window-width="1920"
inkscape:window-height="1531"
inkscape:window-x="20"
inkscape:window-y="20"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<g
id="g21">
<circle
style="fill:#2ec27e;fill-opacity:1;stroke-width:15.5406"
id="circle21"
r="16.271875"
cy="16.933331"
cx="16.933334" />
</g>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath21">
<g
id="g22">
<circle
style="fill:#2ec27e;fill-opacity:1;stroke-width:15.5406"
id="circle22"
r="16.271875"
cy="16.933331"
cx="16.933334" />
</g>
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath22">
<g
id="g23">
<circle
style="fill:#2ec27e;fill-opacity:1;stroke-width:15.5406"
id="circle23"
r="16.271875"
cy="16.933331"
cx="16.933334" />
</g>
</clipPath>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
inkscape:connector-curvature="0"
style="fill:#3d3846;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.854869;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 17.032165,8.0762396 -25.6168937,25.6168934 -0.107123,0.184519 c 0.1452355,0.24999 0.2814119,0.502927 0.4380264,0.748928 0.2914499,0.457721 0.6019592,0.907496 0.9303629,1.347638 0.3283998,0.440177 0.6742854,0.870171 1.0363635,1.288374 0.3620357,0.418161 0.7398069,0.824008 1.1318977,1.216024 0.2769335,0.276839 0.5608148,0.546552 0.8510935,0.808656 0.4109825,0.371156 0.8343648,0.726653 1.2685567,1.065155 0.4342002,0.338532 0.8786706,0.659646 1.3317486,0.962144 0.3735855,0.249412 0.7556397,0.47743 1.13918684,0.700413 L -0.37373862,41.90412 25.243154,16.287229 Z"
id="rect4521"
clip-path="url(#clipPath22)" />
<path
style="fill:#26a269;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.767436;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 10.313989,5.5070223 A 12.376422,12.376422 0 0 0 10.367929,22.955457 12.376422,12.376422 0 0 0 27.80963,23.007897 16.630816,11.941314 45 0 1 26.825988,22.88049 16.630816,11.941314 45 0 1 25.385808,22.554352 16.630816,11.941314 45 0 1 23.93871,22.089311 16.630816,11.941314 45 0 1 22.499094,21.489478 16.630816,11.941314 45 0 1 21.08135,20.761399 16.630816,11.941314 45 0 1 19.699688,19.911985 16.630816,11.941314 45 0 1 18.367939,18.94984 16.630816,11.941314 45 0 1 17.099382,17.884687 16.630816,11.941314 45 0 1 16.248286,17.076028 16.630816,11.941314 45 0 1 15.116392,15.860005 16.630816,11.941314 45 0 1 14.080026,14.571631 16.630816,11.941314 45 0 1 13.149663,13.223993 16.630816,11.941314 45 0 1 12.334743,11.830459 16.630816,11.941314 45 0 1 11.643118,10.404863 16.630816,11.941314 45 0 1 11.081889,8.9616 16.630816,11.941314 45 0 1 10.656669,7.5150653 16.630816,11.941314 45 0 1 10.371662,6.0795606 16.630816,11.941314 45 0 1 10.313989,5.5070223 Z"
id="path4528"
inkscape:connector-curvature="0"
clip-path="url(#clipPath21)" />
<path
style="fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.767436;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10.313527,5.5065594 a 16.630816,11.941314 45 0 0 0.05767,0.572538 16.630816,11.941314 45 0 0 0.285007,1.4355047 16.630816,11.941314 45 0 0 0.425223,1.4465347 16.630816,11.941314 45 0 0 0.561227,1.4432632 16.630816,11.941314 45 0 0 0.691627,1.425596 16.630816,11.941314 45 0 0 0.81492,1.393534 16.630816,11.941314 45 0 0 0.930361,1.347638 16.630816,11.941314 45 0 0 1.036365,1.288374 16.630816,11.941314 45 0 0 1.131895,1.216024 16.630816,11.941314 45 0 0 0.851096,0.808659 16.630816,11.941314 45 0 0 1.268554,1.065152 16.630816,11.941314 45 0 0 1.331751,0.962143 16.630816,11.941314 45 0 0 1.381662,0.849415 16.630816,11.941314 45 0 0 1.417744,0.728082 16.630816,11.941314 45 0 0 1.439617,0.599832 16.630816,11.941314 45 0 0 1.447094,0.465042 16.630816,11.941314 45 0 0 1.440181,0.326135 16.630816,11.941314 45 0 0 0.983644,0.12741 12.376422,12.376422 0 0 0 0.05964,-0.05403 l 0.0062,-0.0062 a 12.376422,12.376422 0 0 0 -0.01073,-17.5013436 12.376422,12.376422 0 0 0 -17.501246,0.00767 12.376422,12.376422 0 0 0 -0.04945,0.052998 z"
id="path4523"
inkscape:connector-curvature="0"
clip-path="url(#clipPath20)" />
<path
style="fill:#2ec27e;fill-opacity:1;stroke-width:7.9375"
d="M 14.96738,-22.579915 20.569667,8.5719967"
id="path15" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

49
typings/mpv.pyi Normal file
View file

@ -0,0 +1,49 @@
from typing import Any, Callable, Iterable, Optional, Protocol
from PIL.Image import Image
class ShutdownError(Exception):
pass
class Unregisterable(Protocol):
def unregister(self) -> None: ...
class ImageOverlay:
overlay_id: int
def remove(self) -> None: ...
class MpvEvent:
def as_dict(self) -> dict[str, bytes]: ...
class MPV:
pause: bool
keep_open: str
image_display_duration: int
sub_pos: int
osd_width: str
osd_height: str
title: str
def __init__(
self, ytdl: bool, input_default_bindings: bool, input_vo_keyboard: bool, osc: bool
) -> None: ...
def terminate(self) -> None: ...
def play(self, file: str) -> None: ...
def playlist_append(self, file: str) -> None: ...
def wait_for_property(self, property: str) -> None: ...
def playlist_next(self) -> None: ...
def audio_add(self, file: str) -> None: ...
def wait_for_event(self, event: str) -> None: ...
def python_stream(
self, stream_name: str
) -> Callable[[Callable[[], Iterable[bytes]]], Unregisterable]: ...
def sub_add(self, file: str) -> None: ...
def create_image_overlay(self, image: Image, pos: tuple[int, int]) -> ImageOverlay: ...
def remove_overlay(self, overlay_id: int) -> None: ...
def observe_property(self, property: str, callback: Callable[[str, Any], None]) -> None: ...
def loadfile(
self, file: str, audio_file: Optional[str] = None, sub_file: Optional[str] = None
) -> None: ...
def register_event_callback(self, callback: Callable[..., Any]) -> None: ...
def __setitem__(self, key: str, value: str) -> None: ...
def __getitem__(self, key: str) -> str: ...

View file

@ -0,0 +1,3 @@
from typing import Literal
def predict(strings: list[str]) -> list[Literal[0] | Literal[1]]: ...

17
typings/qasync.pyi Normal file
View file

@ -0,0 +1,17 @@
from types import TracebackType
from typing import Optional
import PyQt6.QtWidgets
from asyncio import BaseEventLoop
class QApplication(PyQt6.QtWidgets.QApplication):
def __init__(self, argv: list[str]) -> None: ...
class QEventLoop(BaseEventLoop):
def __init__(self, app: QApplication) -> None: ...
def __enter__(self) -> None: ...
def __exit__(
self,
exc_type: Optional[type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None: ...

8
typings/qrcode/main.pyi Normal file
View file

@ -0,0 +1,8 @@
from PIL import Image
class QRCode:
def __init__(self, box_size: int, border: int) -> None: ...
def add_data(self, string: str) -> None: ...
def make(self) -> None: ...
def print_ascii(self) -> None: ...
def make_image(self) -> Image.Image: ...

View file

@ -1,15 +1,11 @@
from typing import Any from typing import Any, Awaitable
from typing import Callable from typing import Callable
from typing import Optional from typing import Optional
from typing import TypeVar from typing import TypeVar, TypeAlias
Handler = TypeVar( Handler: TypeAlias = Callable[[str], Awaitable[Any]]
"Handler", DictHandler: TypeAlias = Callable[[str, dict[str, Any]], Awaitable[Any]]
bound=Callable[[str, dict[str, Any]], Any] | Callable[[str], Any], ClientHandler = TypeVar("ClientHandler", bound=Callable[[dict[str, Any]], Any] | Callable[[], Any])
)
ClientHandler = TypeVar(
"ClientHandler", bound=Callable[[dict[str, Any]], Any] | Callable[[], Any]
)
class _session_context_manager: class _session_context_manager:
async def __aenter__(self) -> dict[str, Any]: ... async def __aenter__(self) -> dict[str, Any]: ...
@ -30,15 +26,20 @@ class AsyncServer:
room: Optional[str] = None, room: Optional[str] = None,
) -> None: ... ) -> None: ...
def session(self, sid: str) -> _session_context_manager: ... def session(self, sid: str) -> _session_context_manager: ...
def on(self, event: str) -> Callable[[Handler], Handler]: ... def on(
self, event: str, handler: Optional[Handler | DictHandler] = None
) -> Callable[[Handler | DictHandler], Handler | DictHandler]: ...
async def enter_room(self, sid: str, room: str) -> None: ... async def enter_room(self, sid: str, room: str) -> None: ...
async def leave_room(self, sid: str, room: str) -> None: ... async def leave_room(self, sid: str, room: str) -> None: ...
def attach(self, app: Any) -> None: ... def attach(self, app: Any) -> None: ...
async def disconnect(self, sid: str) -> None: ... async def disconnect(self, sid: str) -> None: ...
def instrument(self, auth: dict[str, str]) -> None: ...
class AsyncClient: class AsyncClient:
def __init__(self, json: Any = None): ... def __init__(self, json: Any = None): ...
def on(self, event: str) -> Callable[[ClientHandler], ClientHandler]: ... def on(
self, event: str, handler: Optional[Callable[..., Any]] = None
) -> Callable[[ClientHandler], ClientHandler]: ...
async def wait(self) -> None: ... async def wait(self) -> None: ...
async def connect(self, server: str) -> None: ... async def connect(self, server: str) -> None: ...
async def disconnect(self) -> None: ... async def disconnect(self) -> None: ...

View file

@ -0,0 +1,2 @@
class ConnectionError(Exception): ...
class BadNamespaceError(Exception): ...