Compare commits

..

92 Commits

Author SHA1 Message Date
451df57c9d
README: update project url 2025-01-06 16:54:02 +01:00
d10db19d7d flake.lock: Update
Flake lock file updates:

• Updated input 'fix-python':
    'github:GuillaumeDesforges/fix-python/f7f4b33e22414071fc1f9cbf68072c413c3a7fdf' (2024-04-23)
  → 'github:GuillaumeDesforges/fix-python/2926402234c3f99aa8e4608c51d9ffa73ea403c0' (2024-09-04)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/f1010e0469db743d14519a1efd37e23f8513d714' (2024-05-09)
  → 'github:NixOS/nixpkgs/c04d5652cfa9742b1d519688f65d1bbccea9eb7e' (2024-09-19)
2024-09-20 21:51:51 +02:00
b534d7c111 bump sanic, poetry lock 2024-09-20 21:51:45 +02:00
0481aef655
Merge pull request #7 from Programvareverkstedet/ytdl-format
limit ytdl quality to 1080p
2024-05-18 22:58:25 +02:00
891f3a109e limit to 1080p 2024-05-18 17:26:39 +02:00
3841cda1cd fix 2024-05-10 20:03:52 +02:00
df0a64f7c9 Update inputs 2024-05-10 20:03:52 +02:00
25c0f3d6da
Merge pull request #6 from Programvareverkstedet/dependabot/pip/pycryptodomex-3.19.1
Bump pycryptodomex from 3.18.0 to 3.19.1
2024-04-12 00:56:44 +02:00
9eaba26b16 relax yt-dlp
moving upstream target, we always want the newest version
2024-04-12 00:52:31 +02:00
dependabot[bot]
241c69b03a
Bump pycryptodomex from 3.18.0 to 3.19.1
Bumps [pycryptodomex](https://github.com/Legrandin/pycryptodome) from 3.18.0 to 3.19.1.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.18.0...v3.19.1)

---
updated-dependencies:
- dependency-name: pycryptodomex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-05 17:52:00 +00:00
9b9c3ac7d4 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/caac0eb6bdcad0b32cb2522e03e4002c8975c62e' (2023-08-16)
  → 'github:NixOS/nixpkgs/f5892ddac112a1e9b3612c39af1b72987ee5783a' (2023-09-29)
2023-10-03 17:24:25 +02:00
5d7e23c968
Merge pull request #4 from Programvareverkstedet/dependabot/pip/yt-dlp-2023.9.24
Bump yt-dlp from 2023.7.6 to 2023.9.24
2023-10-03 17:23:32 +02:00
dependabot[bot]
08d4f22a58
Bump yt-dlp from 2023.7.6 to 2023.9.24
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2023.7.6 to 2023.9.24.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2023.07.06...2023.09.24)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 17:36:53 +00:00
973c15af7a youtube-dl -> yt-dlp 2023-09-05 00:04:55 +02:00
ab7ff50cc5 tabs -> spaces 2023-09-04 23:24:08 +02:00
63eb73afbf fix 2023-08-27 01:54:59 +02:00
34134cd81c upgrade to sanic-ext 2023-08-27 00:51:10 +02:00
eeaa845dbd module
untested
2023-08-26 23:43:14 +02:00
59fffe853a vroom vroom 2023-08-17 20:39:22 +02:00
1497e3e679 brrr 2022-08-27 22:35:14 +02:00
3e69ac3b91
Merge pull request #3 from Programvareverkstedet/dependabot/pip/sanic-21.12.2
Bump sanic from 21.12.1 to 21.12.2
2022-08-07 03:50:49 +02:00
dependabot[bot]
b5214948ab
Bump sanic from 21.12.1 to 21.12.2
Bumps [sanic](https://github.com/sanic-org/sanic) from 21.12.1 to 21.12.2.
- [Release notes](https://github.com/sanic-org/sanic/releases)
- [Changelog](https://github.com/sanic-org/sanic/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/sanic-org/sanic/compare/v21.12.1...v21.12.2)

---
updated-dependencies:
- dependency-name: sanic
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-06 05:24:17 +00:00
58a889b290
Merge pull request #2 from Programvareverkstedet/dependabot/pip/ujson-5.4.0
Bump ujson from 5.2.0 to 5.4.0
2022-07-06 13:34:09 +02:00
dependabot[bot]
9967d51648
Bump ujson from 5.2.0 to 5.4.0
Bumps [ujson](https://github.com/ultrajson/ultrajson) from 5.2.0 to 5.4.0.
- [Release notes](https://github.com/ultrajson/ultrajson/releases)
- [Commits](https://github.com/ultrajson/ultrajson/compare/5.2.0...5.4.0)

---
updated-dependencies:
- dependency-name: ujson
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-05 22:37:00 +00:00
d4345477ec
Merge pull request #1 from Programvareverkstedet/dependabot/pip/ujson-5.2.0
Bump ujson from 5.1.0 to 5.2.0
2022-05-29 17:11:43 +02:00
dependabot[bot]
9337b275e6
Bump ujson from 5.1.0 to 5.2.0
Bumps [ujson](https://github.com/ultrajson/ultrajson) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/ultrajson/ultrajson/releases)
- [Commits](https://github.com/ultrajson/ultrajson/compare/5.1.0...5.2.0)

---
updated-dependencies:
- dependency-name: ujson
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-26 01:29:39 +00:00
8b42e27e33 MPV class cleanup 2022-02-19 23:04:29 +01:00
fc33b9e1ae Add direnv shorthands, minor cleanup 2022-02-19 23:04:14 +01:00
64d7031711 Track and lock dependencies with poetry 2022-02-19 23:03:34 +01:00
b9968d88e3 cleanup and type hints 2022-02-19 23:00:44 +01:00
1b9c723e06 remove grzegorz.jpg from initial playlist 2021-08-19 22:45:50 +02:00
d4a3365343 fix restricted async python3 contextmanager syntax 2021-08-19 22:45:41 +02:00
904209587d fix mpv call 2021-08-19 22:45:24 +02:00
b14a4e0fd6 update sanic 2021-08-19 22:45:14 +02:00
37a832290e Add ability to go to specific position in the playlist 2019-03-13 12:10:29 +01:00
cced6f8772 Add support for looping the playlist 2019-03-13 11:26:54 +01:00
ebc04f3c07 Allow unlisted playlists 2019-03-13 11:25:39 +01:00
1440c5bdb6 Fix metadata fetch logic issues and typos 2019-03-13 11:25:08 +01:00
9abbebedb0 Fix typo in readme 2019-03-13 11:23:17 +01:00
62feb07233 Whoops, fixed the xorg service file 2019-03-09 21:27:12 +01:00
e7b711b3ad Move config.py to default_config.py and make deplay.bash not delete config.py on the remote 2018-04-12 19:03:36 +02:00
1fc1ab2e1d Prettify and clarify the README 2018-03-06 17:18:17 +01:00
a9129b197a Expand PlaylistDataCache to automatically fetch missing metadata
This is only used if the constructor kwarg "auto_fetch_data" evaluates to True
2018-03-05 23:15:01 +01:00
33be49d2d3 Fix bug in nyasync where it would use the asyncio event loop set at function define time 2018-03-05 23:12:07 +01:00
52245b1256 Fix typos in README 2018-03-05 23:11:55 +01:00
1c7116fd75 Add readme and dist setup 2018-03-04 18:29:08 +01:00
86c830b655 Remove dead code
Was supposed to become a api endpoint, but was merged into api.time_set in an earlier commit
2018-03-04 15:27:20 +01:00
422fa5bb4c Add api.playlist_shuffle 2018-03-04 15:25:39 +01:00
3ce1008db6 Add api.playlist_move 2018-03-04 15:25:19 +01:00
0b64705fdb Implement api.playlist_remove_or_clear
It handles both mpv_control.playlist_remove and mpv_control.playlist_clear
2018-03-04 15:06:21 +01:00
6388a8bfc3 Add proper errorhandling and returnvalues to MPVControl shorthand methods 2018-03-04 14:24:41 +01:00
22c363a788 Add PlaylistDataCache
PlaylistDataCache stores arbitrary data supplied by the client while issuing a loadfile command
The contents of this cache matches with filenames and returns its matched content when any user fetches the playlist
This is mainly used to store metadata.
Could be expanded to having the api automatically fetch the metadata using youtube-dl or something
2018-03-04 03:54:29 +01:00
c0c29974d3 Add ability to seek 2018-02-28 22:18:22 +01:00
6b88676dc4 Add index to playlist and make play_set better 2018-02-28 21:06:28 +01:00
92fcac7689 Add openapi description 2018-02-25 02:40:58 +01:00
de4ce78bad Make __init__ produce a sanic app object instead of simply blocking as main()
Moved test() coro to main.py outside of grzegors module
2018-02-25 00:18:05 +01:00
710d207dbd Ensure "playing" in playlist_get() is true
This must be a bug in MPV
2018-02-24 23:40:08 +01:00
bca1ec78b3 Rename rest methods, edit /load input from body to form argument, add error handling for api input 2018-02-24 23:35:06 +01:00
226e807f24 Add volume control, errorhandling and MPVError exception class 2018-02-24 23:17:05 +01:00
7f6b9fcba9 Implement mpv play/pause, next, previous and get playlist 2018-02-24 21:31:52 +01:00
577959432b Add status check in deployment script 2018-02-24 21:30:43 +01:00
bf9503e43a Add route bases for all MPVControl shorthand command requests in API
This is not yet useable by any means
2018-02-17 00:12:11 +01:00
1744da9158 Add more shorthand command requests to MPVControl
Most of these are untested...
2018-02-17 00:05:58 +01:00
14806b42fb Add basic sanic setup to use for REST api 2018-02-16 23:19:50 +01:00
4f7aebd6fb Add cleanup in MPV class and shorthand command method in MPVControl 2018-02-16 23:18:19 +01:00
694f7aab11 Fix spelling 2018-02-16 23:16:34 +01:00
aac11e7d54 Remove all REMI related things 2018-02-16 17:48:11 +01:00
732b4a0d07 Set remi to v1.0 2018-02-16 17:21:57 +01:00
Aleksander Wasaznik
204d062ae0 Add a main.py file for running the module 2017-02-06 20:30:29 +01:00
Aleksander Wasaznik
ce6f6235d1 Implement a basic MPV controller
Raw commands are supported. Events not yet.
2017-02-06 20:30:29 +01:00
Aleksander Wasaznik
3398af9888 Remove the type odd annotation from playlistmanage 2017-02-06 20:30:29 +01:00
Aleksander Wasaznik
2b33f51053 Use asyncio.gather instead of asyncio.wait
asyncio.gather immediatly propagates exceptions, which is nice for
debugging, and what we wanted in the first place.
2017-02-06 20:30:29 +01:00
Aleksander Wasaznik
2f130874f7 Update deploy to match new systemd setup
Grezgorz now runs as a service on the user instance of systemd.
2017-02-06 20:30:29 +01:00
1fc402d911 Merge branch 'master' of https://github.com/Programvareverkstedet/grzegorz 2017-02-05 17:11:18 +01:00
f39f215c5a updated server.py to use the newest version of REMI and added a nice PVV logo in the background 2017-02-05 17:10:50 +01:00
Aleksander Wasaznik
bc54eb3f6b Add singleton playlist manager
Currently implemented is queing, dequening, and automatic title fetch
using metadatafetch.
2017-02-05 16:30:18 +01:00
Aleksander Wasaznik
f58217d0cb Add missing async in UnixConnection in nyasync 2017-02-05 16:28:40 +01:00
Aleksander Wasaznik
3a329fe689 Implement title fetch in metadatafetch.py
A simple prototype for fetching the title of media using youtube-dl.
2017-02-05 15:59:23 +01:00
Aleksander Wasaznik
2466c20059 Create nyasync library
The library contains helpers for more ergonomic use of asyncio.
2017-02-05 15:59:23 +01:00
Aleksander Wasaznik
f95dc7f3e4 Add skeleton asyncio launcher 2017-02-05 15:59:23 +01:00
Aleksander Wasaznik
5d2b56b015 Improve deploy script
The script now handles filenames with spaces and such. It also removes
any old files from previous deployment.
2017-02-05 15:59:23 +01:00
c2ee159dde started work on playlist management 2017-02-05 15:16:00 +01:00
0ac75ff93a Use tar to deploy 2016-09-13 21:13:46 +02:00
7b85d39629 Make MPV use drm video output 2016-09-13 20:57:45 +02:00
a8ce8246b9 Merge branch 'master' of github.com:Programvareverkstedet/grzegorz 2016-09-13 20:53:14 +02:00
21570efe67 Add deploy script 2016-09-13 20:52:06 +02:00
339256ff67 Introduced the mainfunction and configfile, and added the parameter --no-mpv 2016-09-12 02:12:11 +02:00
8525506744 Make submit button play url in MPV 2016-09-11 21:40:11 +02:00
8a3b42dea3 Add Remi to requirements.txt 2016-09-11 21:29:32 +02:00
62049891d2 Fix gustaebel/python-mpv submodule 2016-09-11 21:23:50 +02:00
891824c363 Merge remote-tracking branch 'origin/mpv-control' 2016-09-11 21:08:15 +02:00
6a5c78742a Started workin the frontend 2016-09-11 20:45:37 +02:00
25 changed files with 2814 additions and 67 deletions

28
.envrc Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# This file is loaded with `direnv`.
# It enters you into the poetry venv, removing the need for `poetry run`.
if command -v nix >/dev/null; then
use flake
fi
export GRZEGORZ_IS_DEBUG=1 # mpv does not start in fullscreen
# Instead of using the flake, we use poetry to manage a development venv
# We only use poetry2nix for deployment
# create venv if it doesn't exist
poetry run true
# enter venv
export VIRTUAL_ENV=$(poetry env info --path)
export POETRY_ACTIVE=1
PATH_add "$VIRTUAL_ENV/bin"
if ! command -v sanic >/dev/null; then
poetry install
# patchelf the venv on nixos
if ! test -s /lib64/ld-linux-x86-64.so.2 || { uname -a | grep -qi nixos; }; then
#nix run github:GuillaumeDesforges/fix-python -- --venv "$VIRTUAL_ENV" #--libs .nix/libs.nix
fix-python --venv "$VIRTUAL_ENV" #--libs .nix/libs.nix
fi
fi

6
.gitignore vendored
View File

@ -1 +1,7 @@
*.pyc
__pycache__
config.py
*.socket
result
result-*
.direnv

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "python-mpv"]
path = lib/python_mpv
url = https://github.com/gustaebel/python-mpv.git

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# Grzegorz API
<img align="right" width="250" src="grzegorz/res/logo.png">
`grzegorz` is simple REST API for managing an instance of MPV.
Why "Grzegorz"? [Great taste in humor of course!](https://youtu.be/t-fcrn1Edik)
When `grzegorz` starts, it launches an instance of MPV and maintains it. It is designed to be used as an info screen or HTPC, and supports multiple users to push changes to the MPV instance.
The API is described and can be tested at `http:/localhost:8080/docs/swagger` while the server is running. All API endpoints are available under `/api`
## How install and run it
Gregorz manages a MPV process, meaning you need to have MPV installed on your system. Look for it in your package manager.
sudo pip install git+https://git.pvv.ntnu.no/Grzegorz/grzegorz#master
sanic grzegorz.app --host :: --port 8080
Details are over [here](https://sanic.dev/en/guide/deployment/running.html#running-via-command).
## Development server
Setup local virtual environment and run with auto-reload:
poetry install
poetry run sanic grzegorz.app --host localhost --port 8000 --debug
The server should now be available at `http://localhost:8000/`.
## A word of caution
Grzegors will make a unix socket in the current working directory. Make sure it is somewhere writeable!
## Making `grzegorz` run on boot
<!-- TODO: make this use Cage or xinit -->
When setting up a info screen or HTPC using Grzegors, you may configure it to run automatically on startup.
We recommend installing a headless linux, and create a user for `grzegorz` to run as. (We named ours `grzegorz`, obviously)
Clone this repo into the home directory. Then make systemd automatically spin up a X session to run `grzegorz` in: Copy the files in the folder `dist` into the folder `$HOME/.config/systemd/user` and run the following commands as your user:
$ systemctl --user enable grzegorz@0.service
$ systemctl --user start grzegorz@0.service

34
deploy.bash Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# My little crappy deploy script
# Uploads all files not ignored by git
TARGET=grzegorz@brzeczyszczykiewicz.pvv.ntnu.no
TARGET_PATH='grzegorz'
array=(); while IFS= read -rd '' item; do array+=("$item"); done < \
<(git status -z --short | grep -z ^? | cut -z -d\ -f2-; git ls-files -z)
files_not_ignored=("${array[@]}")
ssh -T "$TARGET" "
mv -v '$TARGET_PATH/config.py' /tmp/grzegorz_config.py
rm -rfv $TARGET_PATH
mkdir -pv $TARGET_PATH
mv -v /tmp/grzegorz_config.py '$TARGET_PATH/config.py'
"
echo '== Copying files to target: =='
tar -c "${files_not_ignored[@]}" |
ssh -T "$TARGET" "
tar -vxC $TARGET_PATH
"
echo '== DONE: =='
ssh -T "$TARGET" "
systemctl --user restart grzegorz@0
"
sleep 1
ssh -T "$TARGET" "
systemctl --user status grzegorz@0
"

14
dist/grzegorz@.service vendored Normal file
View File

@ -0,0 +1,14 @@
[Unit]
Description=Grzegorz at display %i
Requires=xorg@%i.socket
Requires=xorg@%i.service
After=xorg@%i.socket
After=xorg@%i.service
[Service]
Environment="DISPLAY=:%i"
ExecStart=/usr/bin/python grzegorz/main.py
[Install]
WantedBy=default.target

9
dist/xorg@.service vendored Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=Xorg server at display %i
Requires=xorg@%i.socket
After=xorg@%i.socket
[Service]
ExecStart=/usr/bin/Xorg :%i -nolisten tcp -noreset -verbose 2 "vt${XDG_VTNR}"
SuccessExitStatus=0 1

8
dist/xorg@.socket vendored Normal file
View File

@ -0,0 +1,8 @@
[Unit]
Description=Socket for xorg at display %i
[Socket]
ListenStream=/tmp/.X11-unix/X%i
[Install]
WantedBy=sockets.target

81
flake.lock generated Normal file
View File

@ -0,0 +1,81 @@
{
"nodes": {
"fix-python": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1725463969,
"narHash": "sha256-d3c1TAlIN1PtK+oQP1wO6XbDfmR4SUp/C/4s7G46ARo=",
"owner": "GuillaumeDesforges",
"repo": "fix-python",
"rev": "2926402234c3f99aa8e4608c51d9ffa73ea403c0",
"type": "github"
},
"original": {
"owner": "GuillaumeDesforges",
"repo": "fix-python",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1726755586,
"narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fix-python": "fix-python",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

142
flake.nix Normal file
View File

@ -0,0 +1,142 @@
{
description = "A REST API for managing a MPV instance over via a RPC socket";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.fix-python.url = "github:GuillaumeDesforges/fix-python";
inputs.fix-python.inputs.nixpkgs.follows = "nixpkgs";
#inputs.fix-python.inputs.flake-utils.follows = "flake-utils";
outputs = {
self,
nixpkgs,
fix-python,
...
} @ inputs:
let
forSystems = systems: f: nixpkgs.lib.genAttrs systems (system: f rec {
inherit system;
pkgs = nixpkgs.legacyPackages.${system};
lib = nixpkgs.legacyPackages.${system}.lib;
});
forAllSystems = forSystems [
"x86_64-linux"
"aarch64-linux"
#"riscv64-linux"
];
in {
packages = forAllSystems ({ system, pkgs, ...}: rec {
sanic-ext = with pkgs.python3.pkgs; buildPythonPackage rec {
pname = "sanic-ext";
version = "23.12.0";
src = fetchPypi {
inherit pname version;
hash = "sha256-QvxB5/r6WPO3kPaF892KjeKBRgtBadDpH04RuHR/hFw=";
};
propagatedBuildInputs = [ pyyaml ];
doCheck = false;
};
grzegorz = with pkgs.python3.pkgs; buildPythonPackage {
pname = "grzegorz";
version = (builtins.fromTOML (builtins.readFile ./pyproject.toml)).tool.poetry.version;
format = "pyproject";
src = ./.;
postInstall = ''
'';
nativeBuildInputs = [ poetry-core ];
propagatedBuildInputs = [ setuptools sanic sanic-ext yt-dlp mpv ];
doCheck = false;
};
grzegorz-run = pkgs.writeShellApplication {
name = "grzegorz-run";
runtimeInputs = [ (pkgs.python3.withPackages (ps: [ grzegorz ])) pkgs.mpv ];
text = ''
TOOMANYARGS=()
if test -z "$*"; then
>&2 echo "DEBUG: No args provided, running with defaults...."
TOOMANYARGS=("--host" "::" "--port" "8080")
fi
(
set -x
sanic grzegorz.app "''${TOOMANYARGS[@]}" "$@"
)
'';
};
default = grzegorz;
});
apps = forAllSystems ({ system, pkgs, ...}: {
default.type = "app";
default.program = "${self.packages.${system}.grzegorz-run}/bin/grzegorz-run";
});
devShells = forAllSystems ({ system, pkgs, ...}: rec {
default = pkgs.mkShellNoCC {
packages = with pkgs; [
poetry
python3
# mpv # myust be in sync with system glibc
fix-python.packages.${system}.default
];
shellHook = let
inherit (pkgs) lib;
libs = [
pkgs.stdenv.cc.cc.lib # dlopen libstdc++
#pkgs.glibc
#pkgs.zlib
];
addPath = var: paths: subdir: ''
export ${var}=${lib.makeSearchPath subdir libs}"''${${var}:+:$${var}}"
'';
in ''
export NIX_PATH=nixpkgs=${nixpkgs}"''${NIX_PATH:+:$NIX_PATH}"
${addPath "CPATH" libs "include"}
${addPath "LD_LIBRARY_PATH" libs "lib"}
'';
};
});
nixosModules.grzegorz-kiosk = { config, pkgs, ... }: let
inherit (pkgs) lib;
cfg = config.services.grzegorz;
in {
options.services.grzegorz = {
enable = lib.mkEnableOption (lib.mdDoc "grzegorz");
package = lib.mkPackageOption self.packages.${config.nixpkgs.system} "grzegorz-run" { };
listenAddr = lib.mkOption {
type = lib.types.str;
default = "::";
};
listenPort = lib.mkOption {
type = lib.types.port;
default = 9090;
};
};
config = {
services.cage.enable = true;
services.cage.program = pkgs.writeShellScript "grzegorz-kiosk" ''
cd $(mktemp -d)
${(lib.escapeShellArgs [
"${cfg.package}/bin/grzegorz-run"
"--host" cfg.listenAddr
"--port" cfg.listenPort
])}
'';
services.cage.user = "grzegorz";
users.users."grzegorz".isNormalUser = true;
system.activationScripts = {
base-dirs = {
text = ''
mkdir -p /nix/var/nix/profiles/per-user/grzegorz
'';
deps = [];
};
};
};
};
};
}

47
grzegorz/__init__.py Normal file
View File

@ -0,0 +1,47 @@
from sanic import Sanic
from pathlib import Path
import traceback
from . import mpv
from . import api
__all__ = ["app"]
module_name = __name__.split(".", 1)[0]
app = Sanic(module_name, env_prefix=module_name.upper() + '_')
# api
app.blueprint(api.bp, url_prefix="/api")
app.add_task(api.PLAYLIST_DATA_CACHE.run())
# openapi:
app.ext.openapi.describe("Grzegorz API",
version = "1.0.0.",
description = "The Grzegorz Brzeczyszczykiewicz API, used to control a running mpv instance",
)
# mpv:
async def runMPVControl():
app.config["mpv_control"] = mpv.MPVControl()
try:
await app.config["mpv_control"].run()
except:
traceback.print_exc()
finally:
pass #app.stop()
app.add_task(runMPVControl())
# populate playlist
async def ensure_splash():
here = Path(__file__).parent.resolve()
while not "mpv_control" in app.config:
await None
mpv_control: mpv.MPVControl = app.config["mpv_control"]
playlist = await mpv_control.playlist_get()
if len(playlist) == 0:
print("Adding splash to playlist...")
await mpv_control.loadfile(here / "res/logo.jpg")
app.add_task(ensure_splash())

221
grzegorz/api.py Normal file
View File

@ -0,0 +1,221 @@
from sanic import Request, Blueprint, response
from sanic_ext import openapi
from functools import wraps
from .mpv import MPVControl
from .playlist_data import PlaylistDataCache
bp = Blueprint("grzegorz-api", strict_slashes=True)
# this blueprint assumes a mpv.MPVControl instance is available
# at request.app.config["mpv_control"]
#route decorators:
def response_json(func):
@wraps(func)
async def newfunc(*args, **kwargs):
try:
request = args[0]
mpv_control = request.app.config["mpv_control"]
out = await func(*args, mpv_control, **kwargs)
if "error" not in out:
out["error"] = False
if "request" in out:
del out["request"]
if "mpv_control" in out:
del out["mpv_control"]
return response.json(out)
except Exception as e:
return response.json({
"error": e.__class__.__name__,
"errortext": str(e)
})
return newfunc
def response_text(func):
@wraps(func)
async def newfunc(*args, **kwargs):
body = await func(*args, **kwargs)
return response.text(body)
return newfunc
class APIError(Exception):
pass
# singleton
PLAYLIST_DATA_CACHE = PlaylistDataCache(auto_fetch_data=True)
#routes:
@bp.get("")
@openapi.exclude(True)
@response_text
async def root(request: Request):
return "Hello friend, I hope you're having a lovely day\n"
@bp.post("/load")
@openapi.summary("Add item to playlist")
@openapi.parameter("path", openapi.String(description="Link to the resource to enqueue"), required=True)
@openapi.parameter("body", openapi.Object(description="Any data you want stored with the queued item"), location="body")
@response_json
async def loadfile(request: Request, mpv_control: MPVControl):
if "path" not in request.args:
raise APIError("No query parameter \"path\" provided")
if request.json:
PLAYLIST_DATA_CACHE.add_data(request.args["path"][0], request.json)
success = await mpv_control.loadfile(request.args["path"][0])
return locals()
@bp.get("/play")
@openapi.summary("Check whether the player is paused or playing")
@response_json
async def play_get(request: Request, mpv_control: MPVControl):
value = await mpv_control.pause_get() == False
return locals()
@bp.post("/play")
@openapi.summary("Set whether the player is paused or playing")
@openapi.parameter("play", openapi.Boolean(description="Whether to be playing or not"))
@response_json
async def play_set(request: Request, mpv_control: MPVControl):
if "play" not in request.args:
raise APIError("No query parameter \"play\" provided")
success = await mpv_control \
.pause_set(str(request.args["play"][0]).lower() not in ["true", "1"])
return locals()
@bp.get("/volume")
@openapi.summary("Get the current player volume")
@response_json
async def volume_get(request: Request, mpv_control: MPVControl):
value = await mpv_control.volume_get()
return locals()
@bp.post("/volume")
@openapi.summary("Set the player volume")
@openapi.parameter("volume", openapi.Integer(description="A number between 0 and 100"))
@response_json
async def volume_set(request: Request, mpv_control: MPVControl):
if "volume" not in request.args:
raise APIError("No query parameter \"volume\" provided")
success = await mpv_control \
.volume_set(int(request.args["volume"][0]))
return locals()
@bp.get("/time")
@openapi.summary("Get current playback position")
@response_json
async def time_get(request: Request, mpv_control: MPVControl):
value = {
"current": await mpv_control.time_pos_get(),
"left": await mpv_control.time_remaining_get(),
}
value["total"] = value["current"] + value["left"]
return locals()
@bp.post("/time")
@openapi.summary("Set playback position")
@openapi.parameter("pos", openapi.Float(description="Seconds to seek to"))
@openapi.parameter("percent", openapi.Integer(description="Percent to seek to"))
@response_json
async def time_set(request: Request, mpv_control: MPVControl):
if "pos" in request.args:
success = await mpv_control.seek_absolute(float(request.args["pos"][0]))
elif "percent" in request.args:
success = await mpv_control.seek_percent(int(request.args["percent"][0]))
else:
raise APIError("No query parameter \"pos\" or \"percent\"provided")
return locals()
@bp.get("/playlist")
@openapi.summary("Get the current playlist")
@response_json
async def playlist_get(request: Request, mpv_control: MPVControl):
value = await mpv_control.playlist_get()
value = list(PLAYLIST_DATA_CACHE.add_data_to_playlist(value))
for i, v in enumerate(value):
v["index"] = i
if "current" in v and v["current"] == True:
v["playing"] = await mpv_control.pause_get() == False
if value: del i, v
return locals()
@bp.post("/playlist/next")
@openapi.summary("Skip to the next item in the playlist")
@response_json
async def playlist_next(request: Request, mpv_control: MPVControl):
success = await mpv_control.playlist_next()
return locals()
@bp.post("/playlist/previous")
@openapi.summary("Go back to the previous item in the playlist")
@response_json
async def playlist_previous(request: Request, mpv_control: MPVControl):
success = await mpv_control.playlist_prev()
return locals()
@bp.post("/playlist/goto")
@openapi.summary("Go chosen item in the playlist")
@openapi.parameter("index", openapi.Integer(description="The 0 indexed playlist item to go to"), required=True)
@response_json
async def playlist_goto(request: Request, mpv_control: MPVControl):
if "index" not in request.args:
raise APIError("Missing the required parameter: \"index\"")
success = await mpv_control.playlist_goto(
int(request.args["index"][0]))
return locals()
@bp.delete("/playlist")
@openapi.summary("Clears single item or whole playlist")
@openapi.parameter("index", openapi.Integer(description="Index to item in playlist to remove. If unset, the whole playlist is cleared"))
@response_json
async def playlist_remove_or_clear(request: Request, mpv_control: MPVControl):
if "index" in request.args:
success = await mpv_control.playlist_remove(int(request.args["index"][0]))
action = f"remove #{request.args['index'][0]}"
else:
success = await mpv_control.playlist_clear()
action = "clear all"
return locals()
@bp.post("/playlist/move")
@openapi.summary("Move playlist item to new position")
@openapi.description(
"Move the playlist entry at index1, so that it takes the "
"place of the entry index2. (Paradoxically, the moved playlist "
"entry will not have the index value index2 after moving if index1 "
"was lower than index2, because index2 refers to the target entry, "
"not the index the entry will have after moving.)")
@openapi.parameter("index2", int, required=True)
@openapi.parameter("index1", int, required=True)
@response_json
async def playlist_move(request: Request, mpv_control: MPVControl):
if "index1" not in request.args or "index2" not in request.args:
raise APIError(
"Missing at least one of the required query "
"parameters: \"index1\" and \"index2\"")
success = await mpv_control.playlist_move(
int(request.args["index1"][0]),
int(request.args["index2"][0]))
return locals()
@bp.post("/playlist/shuffle")
@openapi.summary("Clears single item or whole playlist")
@response_json
async def playlist_shuffle(request: Request, mpv_control: MPVControl):
success = await mpv_control.playlist_shuffle()
return locals()
@bp.get("/playlist/loop")
@openapi.summary("See whether it loops the playlist or not")
@response_json
async def playlist_get_looping(request: Request, mpv_control: MPVControl):
value = await mpv_control.playlist_get_looping()
return locals()
@bp.post("/playlist/loop")
@openapi.summary("Sets whether to loop the playlist or not")
@openapi.parameter("loop", openapi.Boolean(description="Whether to be looping or not"), required=True)
@response_json
async def playlist_set_looping(request: Request, mpv_control: MPVControl):
if "loop" not in request.args:
raise APIError("Missing the required parameter: \"loop\"")
success = await mpv_control.playlist_set_looping(
request.args["loop"][0].lower() in ("1", "true", "on", "inf"))
return locals()

54
grzegorz/metadatafetch.py Normal file
View File

@ -0,0 +1,54 @@
from urllib.parse import urlsplit, urlunsplit, parse_qs, urlencode
import yt_dlp as youtube_dl
from yt_dlp.utils import DownloadError
from . import nyasync
@nyasync.ify
def title(url):
ydl = youtube_dl.YoutubeDL()
return ydl.extract_info(url, download=False).get('title')
def filter_query_params(url, allowed=[]):
split_url = urlsplit(url)
qs = parse_qs(split_url.query)
print(qs)
for key in list(qs.keys()):
if key not in allowed:
del qs[key]
return urlunsplit((
split_url.scheme,
split_url.netloc,
split_url.path,
urlencode(qs, doseq=True),
split_url.fragment,
))
@nyasync.ify
def get_youtube_dl_metadata(url, ydl = youtube_dl.YoutubeDL()):
if urlsplit(url).scheme == "":
return None
if urlsplit(url).netloc.lower() in ("www.youtube.com", "youtube.com", "youtu.be"):
#Stop it from doing the whole playlist
url = filter_query_params(url, allowed=["v"])
elif urlsplit(url).scheme == "ytdl":
url = f"https://youtube.com/watch?v={urlsplit(url).netloc}"
try:
resp = ydl.extract_info(url, download=False)
except DownloadError:
return None
#filter and return:
return {k:v for k, v in resp.items() if k in
("uploader", "title", "thumbnail", "description", "duration")}
async def get_metadata(url):
data = await get_youtube_dl_metadata(url)
if data is None:
# (TODO): local ID3 tags
return {"failed": True}
return data

261
grzegorz/mpv.py Normal file
View File

@ -0,0 +1,261 @@
import os
import asyncio
import json
import time
import shlex
import traceback
from typing import List, Optional, Union
from pathlib import Path
from . import nyasync
IS_DEBUG = os.environ.get("GRZEGORZ_IS_DEBUG", "0") != "0"
class MPV:
# TODO: move this to /tmp or /var/run ?
# TODO: make it configurable with an env variable?
_ipc_endpoint = Path(f"mpv_ipc.socket")
def __init__(self):
self.requests = nyasync.Queue()
self.responses = nyasync.Queue()
self.events = nyasync.Queue()
@classmethod
def mpv_command(cls) -> List[str]:
return [
'mpv',
f'--input-ipc-server={str(cls._ipc_endpoint)}',
'--idle',
'--force-window',
*(('--fullscreen',) if not IS_DEBUG else ()),
'--ytdl-format=bestvideo[height<=?1080]',
'--no-terminal',
'--load-unsafe-playlists',
'--keep-open', # Keep last frame of video on end of video
#'--no-input-default-bindings',
]
async def run(self, is_restarted=False, **kw):
if self._ipc_endpoint.is_socket():
print("Socket found, try connecting instead of starting our own mpv!")
self.proc = None # we do not own the socket
await self.connect(**kw)
else:
print("Starting mpv...")
self.proc = await asyncio.create_subprocess_exec(*self.mpv_command())
await asyncio.gather(
self.ensure_running(),
self.connect(**kw),
)
async def connect(self, *, timeout=10):
await asyncio.sleep(0.5)
t = time.time()
while self.is_running and time.time() - t < timeout:
try:
self.ipc_conn = await nyasync.UnixConnection.from_path(str(self._ipc_endpoint))
break
except (FileNotFoundError, ConnectionRefusedError):
continue
await asyncio.sleep(0.1)
else:
if time.time() - t >= timeout:
#raise TimeoutError
# assume the socket is dead, and start our own instance
print("Socket not responding. Will try deleting it and start mpv ourselves!")
self._ipc_endpoint.unlink()
return await self.run()
else:
raise Exception("MPV died before socket connected")
print("Connected to mpv!")
# TODO: in this state we are unable to detect if the connection is lost
self._future_connect = asyncio.gather(
self.process_outgoing(),
self.process_incomming(),
)
await self._future_connect
def _cleanup_connection(self):
assert self.proc is not None # we must own the socket
self._future_connect.cancel() # reduces a lot of errors on exit
if self._ipc_endpoint.is_socket():
self._ipc_endpoint.unlink()
@property
def is_running(self) -> bool:
if self.proc is None: # we do not own the socket
# TODO: can i check the read and writer?
return self._ipc_endpoint.is_socket()
else:
return self.proc.returncode is None
async def ensure_running(self):
await self.proc.wait()
print("MPV suddenly stopped...")
self._cleanup_connection()
await self.run()
async def process_outgoing(self):
async for request in self.requests:
try:
encoded = json.dumps(request).encode('utf-8')
except Exception as e:
print("Unencodable request:", request)
traceback.print_exception(e)
continue
self.ipc_conn.write(encoded)
self.ipc_conn.write(b'\n')
async def process_incomming(self):
async for response in self.ipc_conn:
msg = json.loads(response)
if 'event' in msg:
self.events.put_nowait(msg)
else: # response
self.responses.put_nowait(msg)
class MPVError(Exception):
pass
class MPVControl:
def __init__(self):
self.mpv = MPV()
self.request_lock = asyncio.Lock()
async def run(self):
await asyncio.gather(
self.mpv.run(),
self.process_events(),
)
async def process_events(self):
async for event in self.mpv.events:
# TODO: print?
pass
async def send_request(self, msg):
async with self.request_lock:
# Note: If asyncio.Lock is FIFO, the put can be moved out of the
# critical section. If await is FIFO, the lock is unnessesary. This
# is the safest option.
self.mpv.requests.put_nowait(msg)
return await self.mpv.responses.get()
#other commands:
async def wake_screen(self):
# TODO: use this
# TODO: wayland counterpart
p = await asyncio.create_subprocess_exec(
"xset",
"-display",
os.environ["DISPLAY"],
"dpms",
"force",
"on"
)
code = await p.wait()
#Shorthand command requests:
async def loadfile(self, file: Union[str, Path]):
"appends to playlist and start playback if paused"
if isinstance(file, Path):
file = str(file)
resp = await self.send_request({"command":["loadfile", file, "append-play"]})
return resp["error"] == "success"
async def pause_get(self):
resp = await self.send_request({"command":["get_property", "pause"]})
if "error" in resp and resp["error"] != "success":
raise MPVError("Unable to get whether paused or not: " + resp["error"])
return resp["data"] if "data" in resp else None
async def pause_set(self, state: bool):
resp = await self.send_request({"command":["set_property", "pause", bool(state)]})
return resp["error"] == "success"
async def volume_get(self):
resp = await self.send_request({"command":["get_property", "volume"]})
if "error" in resp and resp["error"] != "success":
raise MPVError("Unable to get volume! " + resp["error"])
return resp["data"] if "data" in resp else None
async def volume_set(self, volume: int):
resp = await self.send_request({"command":["set_property", "volume", volume]})
return resp["error"] == "success"
async def time_pos_get(self):
resp = await self.send_request({"command":["get_property", "time-pos"]})
if "error" in resp and resp["error"] != "success":
raise MPVError("Unable to get time pos: " + resp["error"])
return resp["data"] if "data" in resp else None
async def time_remaining_get(self):
resp = await self.send_request({"command":["get_property", "time-remaining"]})
if "error" in resp and resp["error"] != "success":
raise MPVError("Unable to get time left:" + resp["error"])
return resp["data"] if "data" in resp else None
async def seek_absolute(self, seconds: float):
resp = await self.send_request({"command":["seek", seconds, "absolute"]})
return resp["data"] if "data" in resp else None
async def seek_relative(self, seconds: float):
resp = await self.send_request({"command":["seek", seconds, "relative"]})
return resp["data"] if "data" in resp else None
async def seek_percent(self, percent: float):
resp = await self.send_request({"command":["seek", percent, "absolute-percent"]})
return resp["data"] if "data" in resp else None
async def playlist_get(self):
resp = await self.send_request({"command":["get_property", "playlist"]})
if "error" in resp and resp["error"] != "success":
raise MPVError("Unable to get playlist:" + resp["error"])
return resp["data"] if "data" in resp else None
async def playlist_next(self):
resp = await self.send_request({"command":["playlist-next", "weak"]})
return resp["error"] == "success"
async def playlist_prev(self):
resp = await self.send_request({"command":["playlist-prev", "weak"]})
return resp["error"] == "success"
async def playlist_goto(self, index):
resp = await self.send_request({"command":["set_property", "playlist-pos", index]})
return resp["error"] == "success"
async def playlist_clear(self):
resp = await self.send_request({"command":["playlist-clear"]})
return resp["error"] == "success"
async def playlist_remove(self, index: Optional[int] = None):
resp = await self.send_request({"command":["playlist-remove", "current" if index==None else index]})
return resp["error"] == "success"
async def playlist_move(self, index1: int, index2: int):
resp = await self.send_request({"command":["playlist-move", index1, index2]})
return resp["error"] == "success"
async def playlist_shuffle(self):
resp = await self.send_request({"command":["playlist-shuffle"]})
return resp["error"] == "success"
async def playlist_get_looping(self):
resp = await self.send_request({"command":["get_property", "loop-playlist"]})
return resp["data"] == "inf" if "data" in resp else False
async def playlist_set_looping(self, value: bool):
resp = await self.send_request({"command":["set_property", "loop-playlist", "inf" if value else "no"]})
return resp["error"] == "success"
# CLI entrypoint
def print_mpv_command():
print(*map(shlex.quote, MPV.mpv_command()))

70
grzegorz/nyasync.py Normal file
View File

@ -0,0 +1,70 @@
import asyncio
from asyncio.streams import StreamReader, StreamWriter
def ify(func):
"""Decorate func to run async in default executor"""
async def asyncified(*args, **kwargs):
asyncloop = asyncio.get_event_loop()
return await asyncloop.run_in_executor(
None, lambda: func(*args, **kwargs))
return asyncified
def safely(func, *args, **kwargs):
asyncloop = asyncio.get_event_loop()
asyncloop.call_soon_threadsafe(lambda: func(*args, **kwargs))
def callback(coro, callback=None):
asyncloop = asyncio.get_event_loop()
future = asyncio.run_cooroutine_threadsafe(coro, asyncloop)
if callback:
future.add_done_callback(callback)
return future
def run(*coros):
asyncloop = asyncio.get_event_loop()
return asyncloop.run_until_complete(asyncio.gather(*coros))
class Queue(asyncio.Queue):
__anext__ = asyncio.Queue.get
def __aiter__(self):
return self
class Event:
def __init__(self):
self.monitor = asyncio.Condition()
def __aiter__(self):
return self.monitor.wait()
async def notify(self):
with await self.monitor:
self.monitor.notify_all()
class Condition:
def __init__(self, predicate):
self.predicate = predicate
self.monitor = asyncio.Condition()
def __aiter__(self):
return self.monitor.wait_for(self.predicate)
async def notify(self):
with await self.monitor:
self.monitor.notify_all()
class UnixConnection:
def __init__(self, reader: StreamReader, writer: StreamWriter):
self.reader: StreamReader = reader
self.writer: StreamWriter = writer
@classmethod
async def from_path(cls, path):
endpoints = await asyncio.open_unix_connection(path, limit=2**24) # default is 2**16
return cls(*endpoints)
def __aiter__(self):
return self.reader.__aiter__() # readline
def write(self, data):
self.writer.write(data)

49
grzegorz/playlist_data.py Normal file
View File

@ -0,0 +1,49 @@
from .metadatafetch import get_metadata
from . import nyasync
#Used in api.playlist_get() and api.loadfile()
class PlaylistDataCache:
def __init__(self, auto_fetch_data = False):
self.filepath_data_map = {}
self.auto_fetch_data = auto_fetch_data
self.jobs = None
def add_data(self, filepath, data=None):
if data:
self.filepath_data_map[filepath] = data
async def run(self):
if not self.auto_fetch_data: return
self.jobs = nyasync.Queue()
async for filename in self.jobs:
print("Fetching metadata for ", repr(filename))
data = await get_metadata(filename)
#might already be gone by this point:
if filename in self.filepath_data_map:
self.filepath_data_map[filename].update(data)
del self.filepath_data_map[filename]["fetching"]
def add_data_to_playlist(self, playlist):
seen = set()
for item in playlist:
if "filename" in item:
seen.add(item["filename"])
if item["filename"] in self.filepath_data_map:
new_item = item.copy()
new_item["data"] = self.filepath_data_map[item["filename"]]
yield new_item
continue
elif self.auto_fetch_data:
self.filepath_data_map[item["filename"]] = {"fetching": True}
self.jobs.put_nowait(item["filename"])
new_item = item.copy()
new_item["data"] = {"fetching": True}
yield new_item
continue
yield item
not_seen = set(self.filepath_data_map.keys()) - seen
for name in not_seen:
del self.filepath_data_map[name]

View File

@ -0,0 +1,34 @@
import asyncio
from . import metadatafetch
from . import nyasync
metadatafetch_queue = nyasync.Queue()
async def metadatafetch_loop():
async for item in metadatafetch_queue:
title = await metadatafetch.title(item.url)
item.title = title
metadatafetch_queue.task_done()
class PlaylistItem:
def __init__(self, url):
self.url = url
self.title = None
class Playlist:
def __init__(self):
self.playlist = []
self.nonempty = nyasync.Condition(lambda: self.playlist)
self.change = nyasync.Event()
def queue(self, url):
item = PlaylistItem(url)
self.playlist.append(item)
metadatafetch_queue.put_nowait(item)
self.nonempty.notify()
self.change.notify()
async def dequeue(self):
await self.nonempty
self.change.notify()
return self.playlist.pop(0)

BIN
grzegorz/res/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
grzegorz/res/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

47
grzegorz/res/pvv logo.svg Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 800 800" style="enable-background:new 0 0 800 800;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
.st2{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
.st3{fill:none;}
.st4{stroke:#000000;stroke-miterlimit:10;}
.st5{font-family:'OCRAStd';}
.st6{font-size:126px;}
</style>
<g id="Layer_2">
<rect y="0" class="st0" width="800" height="800"/>
</g>
<g id="Layer_4">
<path class="st1" d="M294.6,720.3"/>
<line class="st1" x1="478.4" y1="720.3" x2="313.2" y2="720.3"/>
<path class="st1" d="M478.4,720.3"/>
<polyline class="st2" points="717.1,223.3 717.1,720.3 497.3,720.3 "/>
<path class="st2" d="M498.3,720.3c0-5.6-4.5-10.1-10.1-10.1c-5.6,0-10.1,4.5-10.1,10.1H314.3c0-5.6-4.5-10.1-10.1-10.1
c-5.6,0-10.1,4.5-10.1,10.1h0.6H76.5V79.7h640.5v120.8v-0.8h-17.3v24.8h17.3"/>
</g>
<g id="Layer_3">
<circle class="st2" cx="396.8" cy="400" r="320.3"/>
</g>
<g id="Layer_1">
<polyline class="st2" points="514.5,173.5 170.2,173.5 170.3,626.6 623.3,626.5 623.3,215.7 584.4,173.4 557,173.4 548,180.6
526.5,180.7 "/>
<path class="st1" d="M396.8,173.5"/>
<path class="st1" d="M396.8,173.3"/>
<path class="st2" d="M526.5,331.8c0,7.6-5.4,13.7-12,13.7H227.7c-6.6,0-12-6.1-12-13.7V187.2c0-7.6,5.4-13.7,12-13.7h286.8
c6.6,0,12,6.1,12,13.7V331.8z"/>
<path class="st2" d="M526.7,333.6c0,6.6-5.4,12-12,12H296.8c-6.6,0-12-5.4-12-12V185.5c0-6.6,5.4-12,12-12h217.9
c6.6,0,12,5.4,12,12V333.6z"/>
<path class="st2" d="M577.9,613.7c0,6.6-5.4,12-12,12H227.7c-6.6,0-12-5.4-12-12V381.1c0-6.6,5.4-12,12-12h338.2
c6.6,0,12,5.4,12,12V613.7z"/>
<rect x="179.9" y="590.2" class="st2" width="25.7" height="23"/>
<rect x="587.6" y="590.2" class="st2" width="25.7" height="23"/>
<rect x="433.6" y="193.5" class="st2" width="64.9" height="137.8"/>
</g>
<g id="Layer_5">
<rect x="258" y="442.5" class="st3" width="277.5" height="109.7"/>
<text transform="matrix(1 0 0 1 260.7021 547.998)" class="st4 st5 st6">PVV</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,22 +0,0 @@
from lib.python_mpv.mpv import MPV as next_MPV
import youtube_dl
ydl = youtube_dl.YoutubeDL()
class MPV(next_MPV):
def __init__(self):
self.default_argv += (
[ '--keep-open'
, '--force-window'
])
super().__init__(debug=True)
def play(self, url):
self.command("loadfile", path)
self.set_property("pause", False)
@staticmethod
def fetchTitle(url):
return ydl.extract_info(url, download=False).get('title')
mpv = MPV()

1639
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
pyproject.toml Normal file
View File

@ -0,0 +1,24 @@
[tool.poetry]
name = "grzegorz"
version = "0.2.0"
description = "A REST API for managing a MPV instance over via a RPC socket."
authors = ["Peder Bergebakken Sundt <pbsds@hotmail.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8,<4.0"
mpv = ">=0.1" # TODO: do we use this?
yt-dlp = ">=2023.9.24"
sanic = ">=23.12.0,<25"
sanic-ext = ">=23.12.0,<24"
#sanic-openapi = ">=21.6.1"
[tool.poetry.dev-dependencies]
python-lsp-server = {extras = ["all"], version = "^1.3.3"}
[tool.poetry.scripts]
grzegorz-mpv-command = 'grzegorz.mpv:print_mpv_command'
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,2 +0,0 @@
mpv==0.1
youtube-dl==2016.9.11.1

View File

@ -1,40 +0,0 @@
import remi.gui as gui
from remi import start, App
class MyApp(App):
def __init__(self, *args):
super(MyApp, self).__init__(*args)
self.pressed = 0
def main(self):
topContainer = gui.HBox()
container = gui.VBox(width=768)
topContainer.append(container)
#playback controls
#playlist
self.lbl = gui.Label('Hello world!')
self.bt = gui.Button('Press me!')
self.asdasd = gui.TextInput(hiehgt=30)
# setting the listener for the onclick event of the button
self.bt.set_on_click_listener(self, 'on_button_pressed')
# appending a widget to another, the first argument is a string key
container.append(self.lbl)
container.append(self.bt)
container.append(self.asdasd)
# returning the root widget
return container
# listener function
def on_button_pressed(self):
self.pressed += 1
self.lbl.set_text('Button pressed %i times!' % self.pressed)
# starts the webserver
start(MyApp, address="0.0.0.0", start_browser=False, multiple_instance=True)