Compare commits
264 commits
more_docum
...
main
Author | SHA1 | Date | |
---|---|---|---|
cd84e90583 | |||
0a16feb5d5 | |||
3eda77012d | |||
7fd54527c8 | |||
8eb484abc2 | |||
15206453ab | |||
b3faae7b10 | |||
a761126eb5 | |||
5ae1b2d7d9 | |||
1d4ae2de84 | |||
6db66b1488 | |||
0165ecd6f0 | |||
33d12db622 | |||
ebc8dd18f5 | |||
dc7b2052ba | |||
56da2ffb1b | |||
66b4f5de95 | |||
3fab9c0402 | |||
a5be76b752 | |||
6672b3d7cd | |||
b954a37a04 | |||
837339df6c | |||
ff5b20bf0d | |||
3623af341f | |||
bea36bcd14 | |||
d7021a934a | |||
0f675a22ad | |||
9ca8df948c | |||
8fc4b2c30c | |||
3acb64e6aa | |||
3c78ff5ad4 | |||
9e0b41505c | |||
bb4607e198 | |||
d247707c11 | |||
5688fef47d | |||
48c02426e0 | |||
e3f7f4d471 | |||
53d6e6efda | |||
284eb8c6b7 | |||
0265ddda71 | |||
607e117325 | |||
c3c0176697 | |||
c2f2682cce | |||
2ae5c7ac06 | |||
421392e1a8 | |||
bf48104acb | |||
6d56fd7def | |||
19c2db9a23 | |||
bd7928a8ca | |||
eb479df689 | |||
8828733271 | |||
c381133252 | |||
9232f3870f | |||
8550387881 | |||
6975aefb23 | |||
57fab12f4e | |||
91dec5bcd9 | |||
25cf546843 | |||
dcab27bf96 | |||
391562fd3a | |||
0df93ac758 | |||
de74d9ecd7 | |||
dfbecc1517 | |||
7488b260e1 | |||
1dcdb8d405 | |||
bdc057026f | |||
7b30100cb7 | |||
56c200fa3a | |||
cc873fc4ff | |||
979e1d385e | |||
02ec988f90 | |||
55a3685c76 | |||
935add9144 | |||
fdcae7edab | |||
dec88d0641 | |||
e4c5f84c00 | |||
d769ed9168 | |||
d2109204f8 | |||
d4af3c0afd | |||
94e0d9c0b7 | |||
e7172a610f | |||
02716a7723 | |||
948bb4da5c | |||
9b5b1ee9d0 | |||
6231d1f47c | |||
3315f95a4d | |||
c6c91d050d | |||
00d85f429b | |||
7fc5b26391 | |||
3fb43de576 | |||
220f1e8779 | |||
55bedf7aa3 | |||
e7b895888b | |||
d21c747927 | |||
65a749b5f8 | |||
95e8157277 | |||
089de1ff93 | |||
bf2e854cdd | |||
d4cf649735 | |||
824198acf6 | |||
f446733bb2 | |||
63555dde87 | |||
e51acd075a | |||
15cc8f8147 | |||
fc9b79172a | |||
b931c4d916 | |||
4320ecd560 | |||
b37dd9f75b | |||
838f91d480 | |||
8144c772f9 | |||
f0d070aff6 | |||
dd12de4a7c | |||
4a8d54e7ba | |||
7ef3eb60f5 | |||
96463a8edb | |||
3a0b360769 | |||
dd348c834a | |||
a99679eda5 | |||
02e77662ac | |||
fdf72ac4f7 | |||
cd3e6d6d7c | |||
8900251b1e | |||
3f6113dab1 | |||
a134a07bff | |||
388d577d0e | |||
c571e82105 | |||
0f3a005792 | |||
6030dc7ff6 | |||
f73eb20955 | |||
fb12bdedd8 | |||
f2e04ab95e | |||
d1700ba75c | |||
3a537760d3 | |||
e4204cee29 | |||
402e8a854d | |||
7d608d99f2 | |||
b16520c555 | |||
806ee45ec2 | |||
d09f81f4e3 | |||
51c6e75522 | |||
a9524b5cf4 | |||
230fc58a06 | |||
aeb41b2eae | |||
528cc357f2 | |||
18322412c2 | |||
a60cc1922d | |||
b12276eb7c | |||
ee85aaa46a | |||
e4155140f1 | |||
f3ecc11eda | |||
3a2d6e43e0 | |||
9395980ecb | |||
e3265b0817 | |||
c78a48bd10 | |||
eb725c7c33 | |||
9279a6a5a2 | |||
73ab2896f9 | |||
b7809c94b8 | |||
dd17c9a91b | |||
2588ec9735 | |||
002f25b60b | |||
18745d18e2 | |||
7b50882738 | |||
829f7b26d1 | |||
1d99945f44 | |||
5a75893aa3 | |||
07cd3c1f96 | |||
9a2cfac637 | |||
bbbf54c9ed | |||
e2895a287c | |||
b960b4365b | |||
6557823d83 | |||
ea6a5d40ad | |||
dd84ff361b | |||
46f68e2e92 | |||
fb05b44b71 | |||
225a605438 | |||
14f9dcb702 | |||
60c3450b3c | |||
2c2c818bfc | |||
e08d108b0b | |||
6472ef4b5a | |||
312fe16cbb | |||
e2686f19bf | |||
152a8ecd0e | |||
ab83ef900e | |||
26024819d6 | |||
0be608ccde | |||
06b400061c | |||
ab4f808296 | |||
4ea76e8787 | |||
58b3837d1b | |||
1cd547a895 | |||
b22f64505e | |||
fdb5310c60 | |||
ea234e6d11 | |||
6501cff7df | |||
3c69e22bc5 | |||
997f7b418c | |||
9bc76795cf | |||
09a9cd34ae | |||
f70d02f75b | |||
4ad4563d80 | |||
27976b05d8 | |||
10239e42f5 | |||
d1f7de5bb7 | |||
3ab15bf04e | |||
391075faaf | |||
b9aa39e5b7 | |||
68f0d0a1f8 | |||
4e77018d7b | |||
4fb3942ce9 | |||
3cb781194d | |||
d3b0b56d57 | |||
af242a9c33 | |||
2aac11a54e | |||
c18c6a152e | |||
efa30022fe | |||
c934a9c4e9 | |||
9a7ebeb22e | |||
![]() |
ff17a6dc7f | ||
de762c170a | |||
b5759b1aa2 | |||
8ef77beaea | |||
2779ab991a | |||
![]() |
5468b39bc1 | ||
![]() |
1f1c2c4f1e | ||
![]() |
bf104362ea | ||
![]() |
b80f6559a7 | ||
![]() |
2d3313e734 | ||
![]() |
20654960cc | ||
![]() |
07bb35e704 | ||
![]() |
d33497d09c | ||
![]() |
fdec53a884 | ||
![]() |
3f68f657bf | ||
![]() |
0a36f657e2 | ||
![]() |
4717d94bdc | ||
![]() |
bb8fe8304e | ||
![]() |
ffa8927859 | ||
![]() |
50585463fc | ||
![]() |
7689172494 | ||
![]() |
85961cf238 | ||
b0b763495f | |||
4ce5df4103 | |||
e2655a483b | |||
6a84567545 | |||
da9ef35ba4 | |||
bcb8843b35 | |||
c78169d669 | |||
411ccdd2c9 | |||
f4e15908cc | |||
14498110ae | |||
1d6e1b8dc8 | |||
ca16dff23b | |||
705169a1f7 | |||
b46d5175cd | |||
d2d316078b | |||
60b0fd42c2 | |||
725beab857 | |||
ae630b7236 | |||
e5a2f88e0b | |||
55939887f3 | |||
56ab58586a | |||
aac8314837 |
123
.github/workflows/build-and-publish.yaml
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -1,4 +1,7 @@
|
|||
docs/build
|
||||
dist
|
||||
__pycache__
|
||||
.venv
|
||||
.idea
|
||||
.flatpak-builder
|
||||
repo
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
image: python:3-alpine
|
||||
|
||||
variables:
|
||||
MYPYPATH: "stubs/"
|
||||
image: python:3.12
|
||||
|
||||
mypy:
|
||||
stage: test
|
||||
script:
|
||||
- pip install mypy types-Pillow types-PyYAML --quiet
|
||||
- mypy syng --strict
|
||||
- pip install poetry
|
||||
- poetry install --all-extras
|
||||
- poetry run mypy syng --strict
|
||||
|
||||
ruff:
|
||||
stage: test
|
||||
script:
|
||||
- pip install ruff --quiet
|
||||
- ruff syng
|
||||
- pip install ruff --quiet
|
||||
- ruff check syng
|
||||
|
|
207
README.md
|
@ -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">
|
||||
|
||||
[](https://matrix.to/#/#syng:matrix.org)
|
||||
[](https://floss.social/@syng)
|
||||
[](https://pypi.org/project/syng/)
|
||||
[](https://flathub.org/apps/rocks.syng.Syng)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
[](https://syng.rocks)
|
||||
[](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.
|
||||
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.
|
||||
- **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](https://mpv.io/) for playback and can therefore play a variety of file formats, such as `mp3+cdg`, `webm`, `mp4`, ...
|
||||
|
||||
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
|
||||
[](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
|
@ -1,47 +1,60 @@
|
|||
[tool.poetry]
|
||||
name = "syng"
|
||||
version = "2.0.0"
|
||||
description = ""
|
||||
version = "2.1.0"
|
||||
description = "Easily host karaoke events"
|
||||
authors = ["Christoph Stahl <christoph.stahl@tu-dortmund.de>"]
|
||||
license = "GPL3"
|
||||
license = "AGPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
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]
|
||||
syng-client = "syng.client:main"
|
||||
syng-server = "syng.server:main"
|
||||
syng-gui = "syng.gui:main"
|
||||
# syng-shell = "syng.webclientmockup:main"
|
||||
syng = "syng.main:main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
python = "^3.9"
|
||||
python-socketio = "^5.10.0"
|
||||
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 }
|
||||
mutagen = { version = "^1.47.0", optional = true }
|
||||
# aiocmd = "^0.1.5"
|
||||
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 }
|
||||
pymediainfo = { version = "^6.1.0", optional = true }
|
||||
pyyaml = { version = "^6.0.1", optional = true }
|
||||
# async-tkinter-loop = "^0.9.2"
|
||||
tkcalendar = { version = "^1.6.1", optional = true }
|
||||
tktimepicker = { version = "^2.0.2", optional = true }
|
||||
platformdirs = { version = "^4.0.0", optional = true }
|
||||
packaging = {version = "^23.2", optional = true}
|
||||
alt-profanity-check = {version = "^1.4.1", optional = true}
|
||||
pyqt6 = {version="^6.7.1", optional = true}
|
||||
mpv = {version = "^1.0.7", optional = true}
|
||||
qasync = {version = "^0.27.1", optional = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
types-pyyaml = "^6.0.12.12"
|
||||
types-pillow = "^10.1.0.2"
|
||||
mypy = "^1.10.0"
|
||||
pylint = "^3.2.7"
|
||||
requirements-parser = "^0.11.0"
|
||||
|
||||
|
||||
[tool.poetry.extras]
|
||||
client = ["minio", "mutagen", "pillow", "yt-dlp",
|
||||
"customtkinter", "qrcode", "pymediainfo", "pyyaml",
|
||||
"tkcalendar", "tktimepicker", "platformdirs", "packaging"]
|
||||
server = ["pytube"]
|
||||
client = ["minio", "pillow", "qrcode", "pymediainfo", "pyyaml", "pyqt6", "mpv", "qasync"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
@ -57,9 +70,13 @@ disable = '''too-many-lines,
|
|||
too-many-ancestors
|
||||
'''
|
||||
|
||||
[tool.mypy]
|
||||
mypy_path = "typings"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"yt_dlp",
|
||||
"yt_dlp.utils",
|
||||
"pymediainfo",
|
||||
"minio",
|
||||
"qrcode",
|
||||
|
@ -74,3 +91,6 @@ ignore_missing_imports = true
|
|||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
|
1061
requirements-client.txt
Normal file
916
requirements-server.txt
Normal 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
|
1516
requirements.txt
9
resources/docker/Dockerfile
Normal 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"]
|
22
resources/flatpak/build_yaml.sh
Normal 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
|
546
resources/flatpak/flatpak-pip-generator
Executable 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))
|
13
resources/flatpak/python3-cffi.yaml
Normal 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
|
12
resources/flatpak/python3-expandvars.yaml
Normal 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:
|
||||
- '*'
|
12
resources/flatpak/python3-poetry-core.yaml
Normal 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:
|
||||
- '*'
|
453
resources/flatpak/python3-requirements-client.yaml
Normal 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
|
187
resources/flatpak/rocks.syng.Syng.metainfo.xml
Normal 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>
|
407
resources/flatpak/rocks.syng.Syng.yaml
Normal 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
|
||||
|
91
resources/icons/eye_clear.svg
Normal 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 |
99
resources/icons/eye_strike.svg
Normal 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 |
BIN
resources/icons/hicolor/128x128/apps/rocks.syng.Syng.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
resources/icons/hicolor/256x256/apps/rocks.syng.Syng.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
resources/icons/hicolor/32x32/apps/rocks.syng.Syng.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
resources/icons/hicolor/48x48/apps/rocks.syng.Syng.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/icons/hicolor/512x512/apps/rocks.syng.Syng.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
resources/icons/hicolor/64x64/apps/rocks.syng.Syng.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
105
resources/icons/hicolor/scalable/apps/rocks.syng.Syng.svg
Normal 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
After Width: | Height: | Size: 94 KiB |
7
resources/resources.qrc
Normal 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>
|
10
resources/rocks.syng.Syng.desktop
Executable 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
|
BIN
resources/screenshots/syng.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
resources/screenshots/syng_advanced.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
resources/screenshots/syng_mobile_search.png
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
resources/screenshots/syng_player_empty.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
resources/screenshots/syng_player_next_up.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
resources/screenshots/syng_player_song.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
resources/screenshots/syng_web.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
resources/screenshots/syng_web2.png
Normal file
After Width: | Height: | Size: 550 KiB |
411
resources/windows/agpl-3.0.rtf
Normal 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 }
|
39
resources/windows/download_and_build.sh
Executable 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
|
57
resources/windows/syng.wxs
Normal 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>
|
|
@ -1,3 +0,0 @@
|
|||
class aiocmd:
|
||||
class PromptToolkitCmd:
|
||||
async def run(self) -> None: ...
|
|
@ -1,6 +0,0 @@
|
|||
class Info:
|
||||
length: int
|
||||
|
||||
class File:
|
||||
def __init__(self, filename: str): ...
|
||||
info: Info
|
|
@ -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: ...
|
|
@ -0,0 +1,2 @@
|
|||
SYNG_VERSION = (2, 1, 1)
|
||||
SYNG_PROTOCOL_VERSION = (2, 1, 1)
|
4
syng/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
856
syng/client.py
|
@ -1,79 +1,71 @@
|
|||
"""
|
||||
Module for the playback client.
|
||||
|
||||
Excerp from the help::
|
||||
|
||||
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: ..
|
||||
The client connects to the server via the socket.io protocol, and plays the
|
||||
songs, that are sent by the server.
|
||||
|
||||
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 os
|
||||
import asyncio
|
||||
import datetime
|
||||
from logging import LogRecord
|
||||
from logging.handlers import QueueHandler
|
||||
from multiprocessing import Queue
|
||||
import secrets
|
||||
import string
|
||||
import tempfile
|
||||
import signal
|
||||
from argparse import ArgumentParser
|
||||
from argparse import Namespace
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from traceback import print_exc
|
||||
from typing import Any, Optional
|
||||
import platformdirs
|
||||
from uuid import UUID
|
||||
|
||||
import qrcode
|
||||
from qrcode.main import QRCode
|
||||
|
||||
import socketio
|
||||
from socketio.exceptions import ConnectionError, BadNamespaceError
|
||||
import engineio
|
||||
from PIL import Image
|
||||
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 .sources import configure_sources, Source
|
||||
|
||||
|
||||
sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder)
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
sources: dict[str, Source] = {}
|
||||
|
||||
|
||||
currentLock: asyncio.Semaphore = asyncio.Semaphore(0)
|
||||
from .log import logger
|
||||
|
||||
|
||||
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 {
|
||||
"server": "http://localhost:8080",
|
||||
"room": "ABCD",
|
||||
"server": "https://syng.rocks",
|
||||
"room": "",
|
||||
"preview_duration": 3,
|
||||
"secret": None,
|
||||
"last_song": 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
|
||||
a room, this must be identical. Also, if a webclient wants to have
|
||||
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
|
||||
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.
|
||||
|
@ -112,6 +105,19 @@ class State:
|
|||
- `optional`, if a performer is already in the queue, they have the option
|
||||
to be put in the waiting room.
|
||||
- `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]:
|
||||
"""
|
||||
|
||||
|
@ -124,323 +130,507 @@ class State:
|
|||
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"]
|
||||
|
||||
|
||||
@sio.on("update_config")
|
||||
async def handle_update_config(data: dict[str, Any]) -> None:
|
||||
state.config = default_config() | data
|
||||
|
||||
|
||||
@sio.on("skip-current")
|
||||
async def handle_skip_current(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "skip-current" message.
|
||||
|
||||
Skips the song, that is currently played. If playback currently waits for
|
||||
buffering, the buffering is also aborted.
|
||||
|
||||
Since the ``queue`` could already be updated, when this evaluates, the
|
||||
first entry in the queue is send explicitly.
|
||||
|
||||
:param data: An entry, that should be equivalent to the first entry of the
|
||||
queue.
|
||||
:rtype: None
|
||||
"""
|
||||
logger.info("Skipping current")
|
||||
if state.current_source is not None:
|
||||
await state.current_source.skip_current(Entry(**data))
|
||||
|
||||
|
||||
@sio.on("state")
|
||||
async def handle_state(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "state" message.
|
||||
|
||||
The "state" message forwards the current queue and recent list from the
|
||||
server. This function saves a copy of both in the global
|
||||
:py:class:`State`:.
|
||||
|
||||
After recieving the new state, a buffering task for the first elements of
|
||||
the queue is started.
|
||||
|
||||
:param data: A dictionary with the `queue` and `recent` list.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
state.queue = [Entry(**entry) for entry in data["queue"]]
|
||||
state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]]
|
||||
state.recent = [Entry(**entry) for entry in data["recent"]]
|
||||
|
||||
for entry in state.queue[:2]:
|
||||
logger.info("Buffering: %s", entry.title)
|
||||
await sources[entry.source].buffer(entry)
|
||||
|
||||
|
||||
@sio.on("connect")
|
||||
async def handle_connect() -> None:
|
||||
"""
|
||||
Handle the "connect" message.
|
||||
|
||||
Called when the client successfully connects or reconnects to the server.
|
||||
Sends a `register-client` message to the server with the initial state and
|
||||
configuration of the client, consiting of the currently saved
|
||||
:py:attr:`State.queue` and :py:attr:`State.recent` field of the global
|
||||
:py:class:`State`, as well a room code the client wants to connect to, a
|
||||
secret to secure the access to the room and a config dictionary.
|
||||
|
||||
If the room code is `None`, the server will issue a room code.
|
||||
|
||||
This message will be handled by the
|
||||
:py:func:`syng.server.handle_register_client` function of the server.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
logging.info("Connected to server")
|
||||
data = {
|
||||
"queue": state.queue,
|
||||
"waiting_room": state.waiting_room,
|
||||
"recent": state.recent,
|
||||
"config": state.config,
|
||||
}
|
||||
await sio.emit("register-client", data)
|
||||
|
||||
|
||||
@sio.on("get-meta-info")
|
||||
async def handle_get_meta_info(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle a "get-meta-info" message.
|
||||
|
||||
Collects the metadata for a given :py:class:`Entry`, from its source, and
|
||||
sends them back to the server in a "meta-info" message. On the server side
|
||||
a :py:func:`syng.server.handle_meta_info` function is called.
|
||||
|
||||
:param data: A dictionary encoding the entry
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
source: Source = sources[data["source"]]
|
||||
meta_info: dict[str, Any] = await source.get_missing_metadata(Entry(**data))
|
||||
await sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info})
|
||||
|
||||
|
||||
async def preview(entry: Entry) -> None:
|
||||
"""
|
||||
Generate and play a preview for a given :py:class:`Entry`.
|
||||
|
||||
This function shows a black screen and prints the artist, title and
|
||||
performer of the entry for a duration.
|
||||
|
||||
This is done by creating a black png file, and showing subtitles in the
|
||||
middle of the screen.... don't ask, it works
|
||||
|
||||
:param entry: The entry to preview
|
||||
:type entry: :py:class:`Entry`
|
||||
:rtype: None
|
||||
"""
|
||||
background = Image.new("RGB", (1280, 720))
|
||||
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,
|
||||
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,
|
||||
)
|
||||
await process.communicate(subtitle.encode())
|
||||
self.register_handlers()
|
||||
self.queue_callbacks: list[Callable[[list[Entry]], None]] = []
|
||||
|
||||
def add_queue_callback(self, callback: Callable[[list[Entry]], None]) -> None:
|
||||
self.queue_callbacks.append(callback)
|
||||
|
||||
@sio.on("play")
|
||||
async def handle_play(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "play" message.
|
||||
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)
|
||||
|
||||
Plays the :py:class:`Entry`, that is encoded in the `data` parameter. If a
|
||||
:py:attr:`State.preview_duration` is set, it shows a small preview before
|
||||
that.
|
||||
def register_handlers(self) -> 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)
|
||||
|
||||
When the playback is done, the next song is requested from the server with
|
||||
a "pop-then-get-next" message. This is handled by the
|
||||
:py:func:`syng.server.handle_pop_then_get_next` function on the server.
|
||||
async def handle_disconnect(self) -> None:
|
||||
logger.info("Disconnected from server")
|
||||
|
||||
If the entry is marked as skipped, emit a "get-first" message instead,
|
||||
because the server already handled the removal of the first entry.
|
||||
async def handle_msg(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "msg" message.
|
||||
|
||||
:param data: A dictionary encoding the entry
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
entry: Entry = Entry(**data)
|
||||
print(
|
||||
f"Playing: {entry.artist} - {entry.title} [{entry.album}] "
|
||||
f"({entry.source}) for {entry.performer}"
|
||||
)
|
||||
try:
|
||||
state.current_source = sources[entry.source]
|
||||
if state.config["preview_duration"] > 0:
|
||||
await preview(entry)
|
||||
await sources[entry.source].play(entry)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
print_exc()
|
||||
state.current_source = None
|
||||
if entry.skip:
|
||||
await sio.emit("get-first")
|
||||
else:
|
||||
await sio.emit("pop-then-get-next")
|
||||
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
|
||||
"""
|
||||
|
||||
@sio.on("client-registered")
|
||||
async def handle_client_registered(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "client-registered" message.
|
||||
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"])
|
||||
|
||||
If the registration was successfull (`data["success"]` == `True`), store
|
||||
the room code in the global :py:class:`State` and print out a link to join
|
||||
the webclient.
|
||||
async def handle_update_config(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "update_config" message.
|
||||
|
||||
Start listing all configured :py:class:`syng.sources.source.Source` to the
|
||||
server via a "sources" message. This message will be handled by the
|
||||
:py:func:`syng.server.handle_sources` function and may request additional
|
||||
configuration for each source.
|
||||
Currently, this function is untested and should be considered dangerous.
|
||||
|
||||
If there is no song playing, start requesting the first song of the queue
|
||||
with a "get-first" message. This will be handled on the server by the
|
||||
:py:func:`syng.server.handle_get_first` function.
|
||||
:param data: A dictionary with the new configuration.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
self.state.config = default_config() | data
|
||||
|
||||
:param data: A dictionary containing a `success` and a `room` entry.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
if data["success"]:
|
||||
logging.info("Registered")
|
||||
print(f"Join here: {state.config['server']}/{data['room']}")
|
||||
qr = qrcode.QRCode(box_size=20, border=2)
|
||||
qr.add_data(f"{state.config['server']}/{data['room']}")
|
||||
qr.make()
|
||||
qr.print_ascii()
|
||||
state.config["room"] = data["room"]
|
||||
await sio.emit("sources", {"sources": list(sources.keys())})
|
||||
if state.current_source is None: # A possible race condition can occur here
|
||||
await sio.emit("get-first")
|
||||
else:
|
||||
logging.warning("Registration failed")
|
||||
await sio.disconnect()
|
||||
async def handle_skip_current(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "skip-current" message.
|
||||
|
||||
Skips the song, that is currently played. If playback currently waits for
|
||||
buffering, the buffering is also aborted.
|
||||
|
||||
@sio.on("request-config")
|
||||
async def handle_request_config(data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "request-config" message.
|
||||
Since the ``queue`` could already be updated, when this evaluates, the
|
||||
first entry in the queue is send explicitly.
|
||||
|
||||
Sends the specific server side configuration for a given
|
||||
:py:class:`syng.sources.source.Source`.
|
||||
:param data: An entry, that should be equivalent to the first entry of the
|
||||
queue.
|
||||
:rtype: None
|
||||
"""
|
||||
logger.info("Skipping current")
|
||||
self.skipped.append(data["uuid"])
|
||||
|
||||
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
|
||||
running enumerator. Otherwise a singe "config" message will be send.
|
||||
entry = Entry(**data)
|
||||
logger.info("Skipping: %s", entry.title)
|
||||
source = self.sources[entry.source]
|
||||
|
||||
:param data: A dictionary with the entry `source` and a string, that
|
||||
corresponds to the name of a source.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
if data["source"] in sources:
|
||||
config: dict[str, Any] | list[dict[str, Any]] = await sources[data["source"]].get_config()
|
||||
if isinstance(config, list):
|
||||
num_chunks: int = len(config)
|
||||
for current, chunk in enumerate(config):
|
||||
await sio.emit(
|
||||
"config-chunk",
|
||||
{
|
||||
"source": data["source"],
|
||||
"config": chunk,
|
||||
"number": current + 1,
|
||||
"total": num_chunks,
|
||||
},
|
||||
)
|
||||
await source.skip_current(Entry(**data))
|
||||
self.player.skip_current()
|
||||
|
||||
async def handle_state(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "state" message.
|
||||
|
||||
The "state" message forwards the current queue and recent list from the
|
||||
server. This function saves a copy of both in the global
|
||||
:py:class:`State`:.
|
||||
|
||||
After recieving the new state, a buffering task for the first elements of
|
||||
the queue is started.
|
||||
|
||||
:param data: A dictionary with the `queue` and `recent` list.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
self.state.queue.clear()
|
||||
self.state.queue.extend([Entry(**entry) for entry in data["queue"]])
|
||||
# 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 pos, entry in enumerate(self.state.queue[0 : self.buffer_in_advance]):
|
||||
source = self.sources[entry.source]
|
||||
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)
|
||||
|
||||
async def handle_connect(self) -> None:
|
||||
"""
|
||||
Handle the "connect" message.
|
||||
|
||||
Called when the client successfully connects or reconnects to the server.
|
||||
Sends a `register-client` message to the server with the initial state and
|
||||
configuration of the client, consiting of the currently saved
|
||||
:py:attr:`State.queue` and :py:attr:`State.recent` field of the global
|
||||
:py:class:`State`, as well a room code the client wants to connect to, a
|
||||
secret to secure the access to the room and a config dictionary.
|
||||
|
||||
If the room code is `None`, the server will issue a room code.
|
||||
|
||||
This message will be handled by the
|
||||
:py:func:`syng.server.handle_register_client` function of the server.
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
logger.info("Connected to server")
|
||||
data = {
|
||||
"queue": self.state.queue,
|
||||
"waiting_room": self.state.waiting_room,
|
||||
"recent": self.state.recent,
|
||||
"config": self.state.config,
|
||||
"version": SYNG_VERSION,
|
||||
}
|
||||
await self.sio.emit("register-client", data)
|
||||
|
||||
async def handle_get_meta_info(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle a "get-meta-info" message.
|
||||
|
||||
Collects the metadata for a given :py:class:`Entry`, from its source, and
|
||||
sends them back to the server in a "meta-info" message. On the server side
|
||||
a :py:func:`syng.server.handle_meta_info` function is called.
|
||||
|
||||
:param data: A dictionary encoding the entry
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
source: Source = self.sources[data["source"]]
|
||||
meta_info: dict[str, Any] = await source.get_missing_metadata(Entry(**data))
|
||||
await self.sio.emit("meta-info", {"uuid": data["uuid"], "meta": meta_info})
|
||||
|
||||
async def preview(self, entry: Entry) -> None:
|
||||
"""
|
||||
Generate and play a preview for a given :py:class:`Entry`.
|
||||
|
||||
This function shows a black screen and prints the artist, title and
|
||||
performer of the entry for a duration.
|
||||
|
||||
This is done by creating a black png file, and showing subtitles in the
|
||||
middle of the screen.... don't ask, it works
|
||||
|
||||
:param entry: The entry to preview
|
||||
:type entry: :py:class:`Entry`
|
||||
:rtype: None
|
||||
"""
|
||||
await self.player.queue_next(entry)
|
||||
|
||||
async def handle_play(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "play" message.
|
||||
|
||||
Plays the :py:class:`Entry`, that is encoded in the `data` parameter. If a
|
||||
:py:attr:`State.preview_duration` is set, it shows a small preview before
|
||||
that.
|
||||
|
||||
When the playback is done, the next song is requested from the server with
|
||||
a "pop-then-get-next" message. This is handled by the
|
||||
:py:func:`syng.server.handle_pop_then_get_next` function on the server.
|
||||
|
||||
If the entry is marked as skipped, emit a "get-first" message instead,
|
||||
because the server already handled the removal of the first entry.
|
||||
|
||||
:param data: A dictionary encoding the entry
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
entry: Entry = Entry(**data)
|
||||
source = self.sources[entry.source]
|
||||
print(
|
||||
f"Playing: {entry.artist} - {entry.title} [{entry.album}] "
|
||||
f"({entry.source}) for {entry.performer}"
|
||||
)
|
||||
if entry.uuid not in self.skipped:
|
||||
try:
|
||||
if self.state.config["preview_duration"] > 0:
|
||||
await self.preview(entry)
|
||||
video, audio = await source.ensure_playable(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
|
||||
print_exc()
|
||||
if self.skipped:
|
||||
self.skipped.remove(entry.uuid)
|
||||
await self.sio.emit("get-first")
|
||||
else:
|
||||
await sio.emit("config", {"source": data["source"], "config": config})
|
||||
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.
|
||||
|
||||
This handles client side search requests. It sends a search request to all
|
||||
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.
|
||||
|
||||
If the registration was successfull (`data["success"]` == `True`), store
|
||||
the room code in the global :py:class:`State` and print out a link to join
|
||||
the webclient.
|
||||
|
||||
Start listing all configured :py:class:`syng.sources.source.Source` to the
|
||||
server via a "sources" message. This message will be handled by the
|
||||
:py:func:`syng.server.handle_sources` function and may request additional
|
||||
configuration for each source.
|
||||
|
||||
If there is no song playing, start requesting the first song of the queue
|
||||
with a "get-first" message. This will be handled on the server by the
|
||||
:py:func:`syng.server.handle_get_first` function.
|
||||
|
||||
:param data: A dictionary containing a `success` and a `room` entry.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
if data["success"]:
|
||||
self.player.start()
|
||||
|
||||
logger.info("Connected to room: %s", 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.print_ascii()
|
||||
|
||||
self.state.config["room"] = data["room"]
|
||||
await self.sio.emit("sources", {"sources": list(self.sources.keys())})
|
||||
if self.state.current_source is None: # A possible race condition can occur here
|
||||
await self.sio.emit("get-first")
|
||||
else:
|
||||
reason = data.get("reason", "Unknown")
|
||||
logger.critical(f"Registration failed: {reason}")
|
||||
await self.sio.disconnect()
|
||||
|
||||
async def handle_request_config(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Handle the "request-config" message.
|
||||
|
||||
Sends the specific server side configuration for a given
|
||||
:py:class:`syng.sources.source.Source`.
|
||||
|
||||
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
|
||||
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
|
||||
corresponds to the name of a source.
|
||||
:type data: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
if data["source"] in self.sources:
|
||||
config: dict[str, Any] | list[dict[str, Any]] = await self.sources[
|
||||
data["source"]
|
||||
].get_config()
|
||||
if isinstance(config, list):
|
||||
num_chunks: int = len(config)
|
||||
for current, chunk in enumerate(config):
|
||||
await self.sio.emit(
|
||||
"config-chunk",
|
||||
{
|
||||
"source": data["source"],
|
||||
"config": chunk,
|
||||
"number": current,
|
||||
"total": num_chunks,
|
||||
},
|
||||
)
|
||||
else:
|
||||
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(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()
|
||||
if self.player.mpv is not None:
|
||||
self.player.mpv.terminate()
|
||||
|
||||
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(self, config: dict[str, Any]) -> None:
|
||||
"""
|
||||
Initialize the client and connect to the server.
|
||||
|
||||
:param config: Config options for the client
|
||||
:type config: dict[str, Any]
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
self.sources.update(configure_sources(config["sources"]))
|
||||
|
||||
if "config" in config:
|
||||
last_song = (
|
||||
datetime.datetime.fromisoformat(config["config"]["last_song"]).timestamp()
|
||||
if "last_song" in config["config"] and config["config"]["last_song"]
|
||||
else None
|
||||
)
|
||||
self.state.config |= config["config"] | {"last_song": last_song}
|
||||
|
||||
if not ("secret" in self.state.config and self.state.config["secret"]):
|
||||
self.state.config["secret"] = "".join(
|
||||
secrets.choice(string.ascii_letters + string.digits) for _ in range(8)
|
||||
)
|
||||
print(f"Generated secret: {self.state.config['secret']}")
|
||||
|
||||
if not ("key" in self.state.config and self.state.config["key"]):
|
||||
self.state.config["key"] = ""
|
||||
|
||||
try:
|
||||
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:
|
||||
pass
|
||||
except ConnectionError:
|
||||
logger.critical("Could not connect to server")
|
||||
finally:
|
||||
self.is_running = False
|
||||
if self.player.mpv is not None:
|
||||
self.player.mpv.terminate()
|
||||
|
||||
|
||||
def signal_handler() -> None:
|
||||
engineio.async_client.async_signal_handler()
|
||||
if state.current_source is not None:
|
||||
if state.current_source.player is not None:
|
||||
state.current_source.player.kill()
|
||||
|
||||
|
||||
async def start_client(config: dict[str, Any]) -> None:
|
||||
def create_async_and_start_client(
|
||||
config: dict[str, Any],
|
||||
queue: Optional[Queue[LogRecord]] = None,
|
||||
client: Optional[Client] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the client and connect to the server.
|
||||
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
|
||||
"""
|
||||
|
||||
sources.update(configure_sources(config["sources"]))
|
||||
if queue is not None:
|
||||
logger.addHandler(QueueHandler(queue))
|
||||
|
||||
if "config" in config:
|
||||
last_song = (
|
||||
datetime.datetime.fromisoformat(config["config"]["last_song"]).timestamp()
|
||||
if "last_song" in config["config"] and config["config"]["last_song"]
|
||||
else None
|
||||
)
|
||||
state.config |= config["config"] | {"last_song": last_song}
|
||||
if client is None:
|
||||
client = Client(config)
|
||||
|
||||
if not ("secret" in state.config and state.config["secret"]):
|
||||
state.config["secret"] = "".join(
|
||||
secrets.choice(string.ascii_letters + string.digits) for _ in range(8)
|
||||
)
|
||||
print(f"Generated secret: {state.config['secret']}")
|
||||
|
||||
if not ("key" in state.config and state.config["key"]):
|
||||
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:
|
||||
await sio.wait()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
if state.current_source is not None:
|
||||
if state.current_source.player is not None:
|
||||
state.current_source.player.kill()
|
||||
asyncio.run(client.start_client(config))
|
||||
|
||||
|
||||
def create_async_and_start_client(config: dict[str, Any]) -> None:
|
||||
asyncio.run(start_client(config))
|
||||
def run_client(args: Namespace) -> None:
|
||||
"""
|
||||
Run the client with the given arguments.
|
||||
|
||||
Namespace contains the following attributes:
|
||||
- room: The room code to connect to
|
||||
- secret: The secret to connect to the room
|
||||
- config_file: The path to the configuration file
|
||||
- key: The key to connect to the server
|
||||
- server: The url of the server to connect to
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point for the syng-client script."""
|
||||
parser: ArgumentParser = ArgumentParser()
|
||||
|
||||
parser.add_argument("--room", "-r")
|
||||
parser.add_argument("--secret", "-s")
|
||||
parser.add_argument(
|
||||
"--config-file",
|
||||
"-C",
|
||||
default=f"{os.path.join(platformdirs.user_config_dir('syng'), 'config.yaml')}",
|
||||
)
|
||||
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:
|
||||
with open(args.config_file, encoding="utf8") as file:
|
||||
config = load(file, Loader=Loader)
|
||||
|
@ -450,7 +640,9 @@ def main() -> None:
|
|||
if "config" not in config:
|
||||
config["config"] = {}
|
||||
|
||||
config["config"] |= {"key": args.key}
|
||||
if "sources" not in config:
|
||||
config["sources"] = {"youtube": {"enabled": True}}
|
||||
|
||||
if args.room:
|
||||
config["config"] |= {"room": args.room}
|
||||
if args.secret:
|
||||
|
@ -459,7 +651,3 @@ def main() -> None:
|
|||
config["config"] |= {"server": args.server}
|
||||
|
||||
create_async_and_start_client(config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
49
syng/config.py
Normal 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]
|
|
@ -1,4 +1,5 @@
|
|||
"""Module for the entry of the queue."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
@ -22,9 +23,9 @@ class Entry:
|
|||
:param duration: The duration of the song in seconds.
|
||||
:type duration: int
|
||||
:param title: The title of the song.
|
||||
:type title: str
|
||||
:type title: Optional[str]
|
||||
: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
|
||||
version is from.
|
||||
:type album: str
|
||||
|
@ -51,8 +52,8 @@ class Entry:
|
|||
ident: str
|
||||
source: str
|
||||
duration: int
|
||||
title: str
|
||||
artist: str
|
||||
title: Optional[str]
|
||||
artist: Optional[str]
|
||||
album: str
|
||||
performer: str
|
||||
failed: bool = False
|
||||
|
@ -60,6 +61,7 @@ class Entry:
|
|||
uuid: UUID = field(default_factory=uuid4)
|
||||
uid: Optional[str] = None
|
||||
started_at: Optional[float] = None
|
||||
incomplete_data: bool = False
|
||||
|
||||
def update(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
|
@ -72,11 +74,19 @@ class Entry:
|
|||
self.__dict__.update(kwargs)
|
||||
|
||||
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]:
|
||||
return set(
|
||||
filter(
|
||||
lambda x: len(x) > 0
|
||||
and x not in ["der", "die", "das", "alle", "und"],
|
||||
lambda x: len(x) > 0 and x not in ["der", "die", "das", "alle", "und"],
|
||||
re.sub(
|
||||
r"[^a-zA-Z0-9\s]",
|
||||
"",
|
||||
|
|
1072
syng/gui.py
|
@ -1,4 +1,5 @@
|
|||
"""Wraps the ``json`` module, so that own classes get encoded."""
|
||||
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
|
3
syng/log.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger("Syng")
|
138
syng/main.py
Normal 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
|
@ -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()
|
|
@ -1,4 +1,5 @@
|
|||
"""A async queue with synchronization."""
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections.abc import Callable, Iterable
|
||||
|
@ -106,6 +107,14 @@ class Queue:
|
|||
updater(item)
|
||||
|
||||
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:
|
||||
if item.shares_performer(name):
|
||||
return item
|
||||
|
@ -172,3 +181,28 @@ class Queue:
|
|||
tmp = self._queue[uuid_idx]
|
||||
self._queue[uuid_idx] = self._queue[uuid_idx - 1]
|
||||
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
|
@ -1,8 +1,9 @@
|
|||
"""Module for search results."""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import os.path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -25,22 +26,21 @@ class Result:
|
|||
ident: str
|
||||
source: str
|
||||
title: str
|
||||
artist: str
|
||||
album: str
|
||||
artist: Optional[str]
|
||||
album: Optional[str]
|
||||
duration: Optional[str] = None
|
||||
|
||||
@staticmethod
|
||||
def from_filename(filename: str, source: str) -> Optional[Result]:
|
||||
@classmethod
|
||||
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::
|
||||
|
||||
{artist} - {title} - {album}.cdg
|
||||
{artist} - {title} - {album}.ext
|
||||
|
||||
Although the extension (cdg) is not required
|
||||
|
||||
If parsing failes, ``None`` is returned. Otherwise a Result object with
|
||||
those attributes is created.
|
||||
If parsing failes, the filename will be used as the title and the
|
||||
artist and album will be set to "Unknown".
|
||||
|
||||
:param filename: The filename to parse
|
||||
:type filename: str
|
||||
|
@ -49,12 +49,68 @@ class Result:
|
|||
:return: see above
|
||||
:rtype: Optional[Result]
|
||||
"""
|
||||
basename = os.path.splitext(filename)[0]
|
||||
try:
|
||||
splitfile = os.path.basename(filename[:-4]).split(" - ")
|
||||
splitfile = os.path.basename(basename).split(" - ")
|
||||
ident = filename
|
||||
artist = splitfile[0].strip()
|
||||
title = splitfile[1].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:
|
||||
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
|
||||
|
|
1992
syng/server.py
|
@ -2,6 +2,7 @@
|
|||
Imports all sources, so that they add themselves to the
|
||||
``available_sources`` dictionary.
|
||||
"""
|
||||
|
||||
# pylint: disable=useless-import-alias
|
||||
|
||||
from typing import Any
|
||||
|
|
|
@ -1,51 +1,75 @@
|
|||
"""Module for an abstract filebased Source."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from syng.entry import Entry
|
||||
|
||||
|
||||
try:
|
||||
from pymediainfo import MediaInfo
|
||||
|
||||
PYMEDIAINFO_AVAILABLE = True
|
||||
except ImportError:
|
||||
if TYPE_CHECKING:
|
||||
from pymediainfo import MediaInfo
|
||||
PYMEDIAINFO_AVAILABLE = False
|
||||
|
||||
from .source import Source
|
||||
from ..config import ListStrOption, ConfigOption
|
||||
|
||||
|
||||
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:
|
||||
-``dir``, dirctory to index and server from.
|
||||
-``extensions``, list of filename extensions
|
||||
"""
|
||||
|
||||
config_schema = Source.config_schema | {
|
||||
"extensions": (
|
||||
list,
|
||||
"extensions": ConfigOption(
|
||||
ListStrOption(),
|
||||
"List of filename extensions\n(mp3+cdg, mp4, ...)",
|
||||
["mp3+cdg"],
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
"""Initialize the file module."""
|
||||
super().__init__(config)
|
||||
|
||||
def apply_config(self, config: dict[str, Any]) -> None:
|
||||
self.build_index = True
|
||||
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:
|
||||
"""Check if a `path` has a correct extension.
|
||||
def is_valid(self, entry: Entry) -> bool:
|
||||
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
|
||||
|
||||
:param path: The path to check.
|
||||
:type path: Optional[str]
|
||||
:return: True iff path has correct extension.
|
||||
: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]]:
|
||||
"""
|
||||
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:]
|
||||
splitted_extensions = [ext.split("+") for ext in self.extensions if "+" in ext]
|
||||
splitted_extensions_dict = {video: audio for [audio, video] in splitted_extensions}
|
||||
|
@ -58,6 +82,14 @@ class FileBasedSource(Source):
|
|||
return (path, None)
|
||||
|
||||
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:
|
||||
return 180
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
"""Module for the files Source."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
from ..entry import Entry
|
||||
from .source import available_sources
|
||||
from .filebased import FileBasedSource
|
||||
from ..config import FolderOption, ConfigOption
|
||||
|
||||
|
||||
class FilesSource(FileBasedSource):
|
||||
|
@ -18,16 +21,13 @@ class FilesSource(FileBasedSource):
|
|||
|
||||
source_name = "files"
|
||||
config_schema = FileBasedSource.config_schema | {
|
||||
"dir": (str, "Directory to index", "."),
|
||||
"index_file": (str, "Index file", "files-index"),
|
||||
"dir": ConfigOption(FolderOption(), "Directory to index", "."),
|
||||
# "index_file": ("file", "Index file", os.path.join(user_cache_dir("syng"), "files-index")),
|
||||
}
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
"""Initialize the file module."""
|
||||
super().__init__(config)
|
||||
|
||||
def apply_config(self, config: dict[str, Any]) -> None:
|
||||
super().apply_config(config)
|
||||
self.dir = config["dir"] if "dir" in config else "."
|
||||
self.extra_mpv_arguments = ["--scale=oversample"]
|
||||
|
||||
async def get_file_list(self) -> list[str]:
|
||||
"""Collect all files in ``dir``, that have the correct filename extension"""
|
||||
|
@ -57,7 +57,7 @@ class FilesSource(FileBasedSource):
|
|||
|
||||
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.
|
||||
|
||||
|
|
|
@ -3,21 +3,28 @@ Construct the S3 source.
|
|||
|
||||
Adds it to the ``available_sources`` with the name ``s3``
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
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:
|
||||
from minio import Minio
|
||||
|
||||
MINIO_AVAILABE = True
|
||||
except ImportError:
|
||||
if TYPE_CHECKING:
|
||||
from minio import Minio
|
||||
MINIO_AVAILABE = False
|
||||
|
||||
from ..entry import Entry
|
||||
from .filebased import FileBasedSource
|
||||
from .source import available_sources
|
||||
from ..config import BoolOption, ConfigOption, FileOption, FolderOption, PasswordOption, StrOption
|
||||
|
||||
|
||||
class S3Source(FileBasedSource):
|
||||
|
@ -27,7 +34,7 @@ class S3Source(FileBasedSource):
|
|||
- ``endpoint``, ``access_key``, ``secret_key``, ``secure``, ``bucket``: These
|
||||
will simply be forwarded to the ``minio`` client.
|
||||
- ``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
|
||||
files from the s3 instance to this file. If it exists, loads
|
||||
the list of files from this file.
|
||||
|
@ -35,19 +42,23 @@ class S3Source(FileBasedSource):
|
|||
|
||||
source_name = "s3"
|
||||
config_schema = FileBasedSource.config_schema | {
|
||||
"endpoint": (str, "Endpoint of the s3", ""),
|
||||
"access_key": (str, "Access Key of the s3", ""),
|
||||
"secret_key": (str, "Secret Key of the s3", ""),
|
||||
"secure": (bool, "Use SSL", True),
|
||||
"bucket": (str, "Bucket of the s3", ""),
|
||||
"tmp_dir": (str, "Folder for\ntemporary download", "/tmp/syng"),
|
||||
"index_file": (str, "Index file", "s3-index"),
|
||||
"endpoint": ConfigOption(StrOption(), "Endpoint of the s3", ""),
|
||||
"access_key": ConfigOption(StrOption(), "Access Key of the s3 (username)", ""),
|
||||
"secret_key": ConfigOption(PasswordOption(), "Secret Key of the s3 (password)", ""),
|
||||
"secure": ConfigOption(BoolOption(), "Use SSL", True),
|
||||
"bucket": ConfigOption(StrOption(), "Bucket of the s3", ""),
|
||||
"tmp_dir": ConfigOption(
|
||||
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]):
|
||||
"""Create the source."""
|
||||
super().__init__(config)
|
||||
|
||||
def apply_config(self, config: dict[str, Any]) -> None:
|
||||
super().apply_config(config)
|
||||
if (
|
||||
MINIO_AVAILABE
|
||||
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.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]:
|
||||
"""
|
||||
|
@ -83,18 +119,29 @@ class S3Source(FileBasedSource):
|
|||
with open(self.index_file, "r", encoding="utf8") as index_file_handle:
|
||||
return cast(list[str], load(index_file_handle))
|
||||
|
||||
file_list = [
|
||||
obj.object_name
|
||||
for obj in self.minio.list_objects(self.bucket, recursive=True)
|
||||
if self.has_correct_extension(obj.object_name)
|
||||
]
|
||||
file_list = self.load_file_list_from_server()
|
||||
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:
|
||||
dump(file_list, index_file_handle)
|
||||
self.write_index(file_list)
|
||||
|
||||
return 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]:
|
||||
"""
|
||||
Return the duration for the music file.
|
||||
|
@ -114,7 +161,7 @@ class S3Source(FileBasedSource):
|
|||
|
||||
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.
|
||||
|
||||
|
@ -135,8 +182,9 @@ class S3Source(FileBasedSource):
|
|||
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:
|
||||
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(
|
||||
asyncio.to_thread(self.minio.fget_object, self.bucket, audio_path, audio_dl_path)
|
||||
|
|
|
@ -4,10 +4,10 @@ Abstract class for sources.
|
|||
Also defines the dictionary of available sources. Each source should add itself
|
||||
to this dictionary in its module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os.path
|
||||
import shlex
|
||||
from collections import defaultdict
|
||||
|
@ -21,10 +21,15 @@ from typing import Tuple
|
|||
from typing import Type
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
from ..log import logger
|
||||
from ..entry import Entry
|
||||
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
|
||||
|
@ -45,9 +50,6 @@ class DLFilesEntry:
|
|||
:param complete: True if download was completed, False otherwise (Default
|
||||
is ``False``)
|
||||
: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
|
||||
(Default is ``False``)
|
||||
:param buffer_task: Reference to the task, that downloads the files.
|
||||
|
@ -61,7 +63,6 @@ class DLFilesEntry:
|
|||
audio: Optional[str] = None
|
||||
buffering: bool = False
|
||||
complete: bool = False
|
||||
failed: bool = False
|
||||
skip: bool = False
|
||||
buffer_task: Optional[asyncio.Task[Tuple[str, Optional[str]]]] = None
|
||||
|
||||
|
@ -75,7 +76,6 @@ class Source(ABC):
|
|||
attribute.
|
||||
|
||||
Source specific tasks will be forwarded to the respective source, like:
|
||||
- Playing the audio/video
|
||||
- Buffering the audio/video
|
||||
- Searching for a query
|
||||
- Getting an entry from an identifier
|
||||
|
@ -88,7 +88,7 @@ class Source(ABC):
|
|||
``get_entry``, ``search``, ``add_to_config``
|
||||
|
||||
Specific client methods:
|
||||
``buffer``, ``do_buffer``, ``play``, ``skip_current``, ``ensure_playable``,
|
||||
``buffer``, ``do_buffer``, ``skip_current``, ``ensure_playable``,
|
||||
``get_missing_metadata``, ``get_config``
|
||||
|
||||
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`.
|
||||
- ``player``, the reference to the ``mpv`` process, if it has
|
||||
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
|
||||
- ``source_name``, the string used to identify the source
|
||||
"""
|
||||
|
||||
source_name: str = ""
|
||||
config_schema: dict[str, tuple[type | list[type], str, Any]] = {
|
||||
"enabled": (bool, "Enable this source", False)
|
||||
config_schema: dict[str, ConfigOption[Any]] = {
|
||||
"enabled": ConfigOption(BoolOption(), "Enable this source", False)
|
||||
}
|
||||
|
||||
def __init__(self, config: dict[str, Any]):
|
||||
|
@ -120,41 +120,36 @@ class Source(ABC):
|
|||
source for documentation.
|
||||
:type config: dict[str, Any]
|
||||
"""
|
||||
self.config: dict[str, Any] = config
|
||||
self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry)
|
||||
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.extra_mpv_arguments: list[str] = []
|
||||
self.extra_mpv_options: dict[str, str] = {}
|
||||
self._skip_next = False
|
||||
self.build_index = False
|
||||
self.apply_config(config)
|
||||
|
||||
@staticmethod
|
||||
async def play_mpv(
|
||||
video: str, audio: Optional[str], /, *options: str
|
||||
) -> asyncio.subprocess.Process:
|
||||
def is_valid(self, entry: Entry) -> bool:
|
||||
"""
|
||||
Create a mpv process to play a song in full screen.
|
||||
Check if the entry is valid.
|
||||
|
||||
:param video: Location of the video part.
|
||||
:type video: str
|
||||
:param audio: Location of the audio part, if it exists.
|
||||
:type audio: Optional[str]
|
||||
:param options: Extra arguments forwarded to the mpv player
|
||||
:type options: str
|
||||
:returns: An async reference to the process
|
||||
:rtype: asyncio.subprocess.Process
|
||||
Each source can implement this method to check if the entry is valid.
|
||||
|
||||
:param entry: The entry to check
|
||||
:type entry: Entry
|
||||
:returns: True if the entry is valid, False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else [])
|
||||
return True
|
||||
|
||||
print(f"File is {video=} and {audio=}")
|
||||
|
||||
mpv_process = asyncio.create_subprocess_exec(
|
||||
"mpv",
|
||||
*args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
)
|
||||
return await mpv_process
|
||||
|
||||
async def get_entry(self, performer: str, ident: str) -> Optional[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` from a given identifier.
|
||||
|
||||
|
@ -173,22 +168,23 @@ class Source(ABC):
|
|||
:returns: New entry for the identifier, or None, if the ident is
|
||||
invalid.
|
||||
: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)
|
||||
if res is not None:
|
||||
return Entry(
|
||||
ident=ident,
|
||||
source=self.source_name,
|
||||
duration=180,
|
||||
album=res.album,
|
||||
title=res.title,
|
||||
artist=res.artist,
|
||||
performer=performer,
|
||||
)
|
||||
return None
|
||||
res: Result = Result.from_filename(ident, self.source_name)
|
||||
entry = Entry(
|
||||
ident=ident,
|
||||
source=self.source_name,
|
||||
duration=180,
|
||||
album=res.album if res.album else "Unknown",
|
||||
title=res.title if res.title else title if title else "Unknown",
|
||||
artist=res.artist if res.artist else artist if artist else "Unknown",
|
||||
performer=performer,
|
||||
incomplete_data=True,
|
||||
)
|
||||
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]:
|
||||
"""
|
||||
|
@ -204,14 +200,11 @@ class Source(ABC):
|
|||
filtered: list[str] = self.filter_data_by_query(query, self._index)
|
||||
results: list[Result] = []
|
||||
for filename in filtered:
|
||||
result: Optional[Result] = Result.from_filename(filename, self.source_name)
|
||||
if result is None:
|
||||
continue
|
||||
results.append(result)
|
||||
results.append(Result.from_filename(filename, self.source_name))
|
||||
return results
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -224,11 +217,13 @@ class Source(ABC):
|
|||
|
||||
:param entry: The entry to buffer
|
||||
: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.
|
||||
: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.
|
||||
|
||||
|
@ -242,6 +237,8 @@ class Source(ABC):
|
|||
|
||||
:param entry: The entry to buffer
|
||||
:type entry: Entry
|
||||
:param pos: The position in the queue, the entry is at.
|
||||
:type pos: int
|
||||
:rtype: None
|
||||
"""
|
||||
async with self._masterlock:
|
||||
|
@ -250,54 +247,21 @@ class Source(ABC):
|
|||
self.downloaded_files[entry.ident].buffering = True
|
||||
|
||||
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
|
||||
video, audio = await buffer_task
|
||||
|
||||
self.downloaded_files[entry.ident].video = video
|
||||
self.downloaded_files[entry.ident].audio = audio
|
||||
self.downloaded_files[entry.ident].complete = True
|
||||
except ValueError as exc:
|
||||
raise exc
|
||||
except Exception: # pylint: disable=broad-except
|
||||
print_exc()
|
||||
logger.error("Buffering failed for %s", entry)
|
||||
self.downloaded_files[entry.ident].failed = True
|
||||
raise ValueError("Buffering failed for %s" % entry)
|
||||
|
||||
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:
|
||||
"""
|
||||
Skips first song in the queue.
|
||||
|
@ -318,10 +282,7 @@ class Source(ABC):
|
|||
buffer_task.cancel()
|
||||
self.downloaded_files[entry.ident].ready.set()
|
||||
|
||||
if self.player is not None:
|
||||
self.player.kill()
|
||||
|
||||
async def ensure_playable(self, entry: Entry) -> None:
|
||||
async def ensure_playable(self, entry: Entry) -> tuple[str, Optional[str]]:
|
||||
"""
|
||||
Guaranties that the given entry can be played.
|
||||
|
||||
|
@ -331,8 +292,10 @@ class Source(ABC):
|
|||
:type entry: Entry
|
||||
:rtype: None
|
||||
"""
|
||||
await self.buffer(entry)
|
||||
await self.downloaded_files[entry.ident].ready.wait()
|
||||
await self.buffer(entry, 0)
|
||||
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]:
|
||||
"""
|
||||
|
@ -384,6 +347,46 @@ class Source(ABC):
|
|||
"""
|
||||
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]]:
|
||||
"""
|
||||
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.
|
||||
:rtype: dict[str, Any] | list[dict[str, Any]]
|
||||
"""
|
||||
if not self._index:
|
||||
self._index = []
|
||||
print(f"{self.source_name}: generating index")
|
||||
self._index = await self.get_file_list()
|
||||
print(f"{self.source_name}: done")
|
||||
chunked = zip_longest(*[iter(self._index)] * 1000, fillvalue="")
|
||||
return [{"index": list(filter(lambda x: x != "", chunk))} for chunk in chunked]
|
||||
packages = []
|
||||
if self.build_index:
|
||||
if not self._index:
|
||||
self._index = []
|
||||
logger.warning(f"{self.source_name}: generating index")
|
||||
self._index = await self.get_file_list()
|
||||
logger.warning(f"{self.source_name}: done")
|
||||
chunked = zip_longest(*[iter(self._index)] * 1000, fillvalue="")
|
||||
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.
|
||||
|
||||
|
@ -420,11 +437,30 @@ class Source(ABC):
|
|||
In the default configuration, this just adds the index key of the
|
||||
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.
|
||||
: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
|
||||
"""
|
||||
self._index += config["index"]
|
||||
pass
|
||||
|
||||
|
||||
available_sources: dict[str, Type[Source]] = {}
|
||||
|
|
|
@ -1,36 +1,167 @@
|
|||
"""
|
||||
Construct the YouTube source.
|
||||
|
||||
If available, downloading will be performed via yt-dlp, if not, pytube will be
|
||||
used.
|
||||
This source uses yt-dlp to search and download videos from YouTube.
|
||||
|
||||
Adds it to the ``available_sources`` with the name ``youtube``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import shlex
|
||||
from functools import partial
|
||||
from urllib.parse import urlencode
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
try:
|
||||
from pytube import Channel, Search, YouTube, exceptions, innertube
|
||||
from yt_dlp import YoutubeDL
|
||||
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 ..result import Result
|
||||
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):
|
||||
|
@ -41,94 +172,113 @@ class YoutubeSource(Source):
|
|||
Examples are ``/c/CCKaraoke`` or
|
||||
``/channel/UCwTRjvjVge51X-ILJ4i22ew``
|
||||
- ``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
|
||||
downloaded/streamed. Default is 720.
|
||||
- ``start_streaming``: If set to ``True``, the client starts streaming
|
||||
the video, if buffering was not completed. Needs ``youtube-dl`` or
|
||||
``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"
|
||||
config_schema = Source.config_schema | {
|
||||
"channels": (list, "A list channels\nto search in", []),
|
||||
"tmp_dir": (str, "Folder for\ntemporary download", "/tmp/syng"),
|
||||
"max_res": (int, "Maximum resolution\nto download", 720),
|
||||
"start_streaming": (
|
||||
bool,
|
||||
"enabled": ConfigOption(BoolOption(), "Enable this source", True),
|
||||
"channels": ConfigOption(
|
||||
ListStrOption(), "A list channels\nto search in", [], send_to_server=True
|
||||
),
|
||||
"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",
|
||||
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 __init__(self, config: dict[str, Any]):
|
||||
"""Create the source."""
|
||||
super().__init__(config)
|
||||
|
||||
if PYTUBE_AVAILABLE:
|
||||
self.innertube_client: innertube.InnerTube = innertube.InnerTube(client="WEB")
|
||||
def apply_config(self, config: dict[str, Any]) -> None:
|
||||
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.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 = (
|
||||
config["start_streaming"] if "start_streaming" in config else False
|
||||
)
|
||||
self.formatstring = (
|
||||
f"bestvideo[height<={self.max_res}]+" f"bestaudio/best[height<={self.max_res}]"
|
||||
)
|
||||
if YT_DLP_AVAILABLE:
|
||||
self._yt_dlp = YoutubeDL(
|
||||
params={
|
||||
"paths": {"home": self.tmp_dir},
|
||||
"format": self.formatstring,
|
||||
"quiet": True,
|
||||
}
|
||||
)
|
||||
self.search_suffix = config.get("search_suffix", "karaoke")
|
||||
self.extra_mpv_options = {"ytdl-format": self.formatstring}
|
||||
self._yt_dlp = YoutubeDL(
|
||||
params={
|
||||
"paths": {"home": self.tmp_dir},
|
||||
"format": self.formatstring,
|
||||
"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
|
||||
:rtype: dict[str, Any]]
|
||||
"""
|
||||
return {"channels": self.channels}
|
||||
If the entry is not yet downloaded, download it.
|
||||
If start_streaming is set, start streaming immediatly.
|
||||
|
||||
async def play(self, entry: Entry) -> None:
|
||||
"""
|
||||
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.
|
||||
:param entry: The entry to download.
|
||||
:type entry: Entry
|
||||
: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.
|
||||
|
||||
The identifier should be a youtube url. An entry is created with
|
||||
all available metadata for the video.
|
||||
|
||||
:param performer: The persong singing.
|
||||
:param performer: The person singing.
|
||||
:type performer: str
|
||||
:param ident: A url to a YouTube video.
|
||||
:type ident: str
|
||||
|
@ -136,37 +286,24 @@ class YoutubeSource(Source):
|
|||
: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(
|
||||
ident=url,
|
||||
source="youtube",
|
||||
album="YouTube",
|
||||
duration=length,
|
||||
title=yt_song.title,
|
||||
artist=yt_song.author,
|
||||
performer=performer,
|
||||
)
|
||||
except exceptions.PytubeError:
|
||||
return None
|
||||
|
||||
return await asyncio.to_thread(_get_entry, performer, ident)
|
||||
return Entry(
|
||||
ident=ident,
|
||||
source="youtube",
|
||||
duration=180,
|
||||
album="YouTube",
|
||||
title=title,
|
||||
artist=artist,
|
||||
performer=performer,
|
||||
incomplete_data=True,
|
||||
)
|
||||
|
||||
async def search(self, query: str) -> list[Result]:
|
||||
"""
|
||||
Search YouTube and the configured channels for the query.
|
||||
|
||||
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"
|
||||
is appended to the search query.
|
||||
results are the results from youtube as a whole, a configurable suffix
|
||||
is appended to the search query (default is "karaoke").
|
||||
|
||||
All results are sorted by how good they match to the search query,
|
||||
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:
|
||||
"""
|
||||
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()
|
||||
hits: int = 0
|
||||
queries: list[str] = shlex.split(query.lower())
|
||||
|
@ -205,70 +353,56 @@ class YoutubeSource(Source):
|
|||
title=result.title,
|
||||
artist=result.author,
|
||||
album="YouTube",
|
||||
duration=str(result.length),
|
||||
)
|
||||
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]:
|
||||
"""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
|
||||
if results is not None:
|
||||
return results
|
||||
return []
|
||||
suffix = f" {self.search_suffix}" if self.search_suffix else ""
|
||||
return Search(f"{query}{suffix}").results
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def _channel_search(self, query: str, channel: str) -> list[YouTube]:
|
||||
"""
|
||||
Search a channel for a query.
|
||||
|
||||
A lot of black Magic happens here.
|
||||
"""
|
||||
browse_id: str = Channel(f"https://www.youtube.com{channel}").channel_id
|
||||
endpoint: str = f"{self.innertube_client.base_url}/browse"
|
||||
return Search(f"{query} karaoke", channel).results
|
||||
|
||||
data: dict[str, str] = {
|
||||
"query": query,
|
||||
"browseId": browse_id,
|
||||
"params": "EgZzZWFyY2g%3D",
|
||||
}
|
||||
data.update(self.innertube_client.base_data)
|
||||
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"]
|
||||
async def get_missing_metadata(self, entry: Entry) -> dict[str, Any]:
|
||||
"""
|
||||
Video metadata should be read on the client to avoid banning
|
||||
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,
|
||||
}
|
||||
return {}
|
||||
|
||||
list_of_videos: list[YouTube] = []
|
||||
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]]:
|
||||
async def do_buffer(self, entry: Entry, pos: int) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Download the video.
|
||||
|
||||
|
@ -279,12 +413,26 @@ class YoutubeSource(Source):
|
|||
location exists, the return value for the audio part will always be
|
||||
``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.
|
||||
: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``.
|
||||
: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"]
|
||||
return combined_path, None
|
||||
|
||||
|
|
598
syng/static/assets/index.520c2769.js
Normal file
1
syng/static/assets/index.ed7016c8.css
Normal file
BIN
syng/static/background.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
syng/static/background20perc.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Syng Rocks!</title>
|
||||
<script type="module" crossorigin src="/assets/index.20e81f9f.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.b030f504.css">
|
||||
<script type="module" crossorigin src="/assets/index.520c2769.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.ed7016c8.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 11 KiB |
105
syng/static/syng.svg
Normal 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
|
@ -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: ...
|
3
typings/profanity_check.pyi
Normal 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
|
@ -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
|
@ -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: ...
|
|
@ -1,15 +1,11 @@
|
|||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import TypeVar
|
||||
from typing import TypeVar, TypeAlias
|
||||
|
||||
Handler = TypeVar(
|
||||
"Handler",
|
||||
bound=Callable[[str, dict[str, Any]], Any] | Callable[[str], Any],
|
||||
)
|
||||
ClientHandler = TypeVar(
|
||||
"ClientHandler", bound=Callable[[dict[str, Any]], Any] | Callable[[], Any]
|
||||
)
|
||||
Handler: TypeAlias = Callable[[str], Awaitable[Any]]
|
||||
DictHandler: TypeAlias = Callable[[str, dict[str, Any]], Awaitable[Any]]
|
||||
ClientHandler = TypeVar("ClientHandler", bound=Callable[[dict[str, Any]], Any] | Callable[[], Any])
|
||||
|
||||
class _session_context_manager:
|
||||
async def __aenter__(self) -> dict[str, Any]: ...
|
||||
|
@ -30,15 +26,20 @@ class AsyncServer:
|
|||
room: Optional[str] = None,
|
||||
) -> None: ...
|
||||
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 leave_room(self, sid: str, room: str) -> None: ...
|
||||
def attach(self, app: Any) -> None: ...
|
||||
async def disconnect(self, sid: str) -> None: ...
|
||||
def instrument(self, auth: dict[str, str]) -> None: ...
|
||||
|
||||
class AsyncClient:
|
||||
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 connect(self, server: str) -> None: ...
|
||||
async def disconnect(self) -> None: ...
|
2
typings/socketio/exceptions.pyi
Normal file
|
@ -0,0 +1,2 @@
|
|||
class ConnectionError(Exception): ...
|
||||
class BadNamespaceError(Exception): ...
|