Compare commits
No commits in common. "master" and "mpv-control" have entirely different histories.
master
...
mpv-contro
28
.envrc
28
.envrc
@ -1,28 +0,0 @@
|
||||
#!/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
6
.gitignore
vendored
@ -1,7 +1 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
config.py
|
||||
*.socket
|
||||
result
|
||||
result-*
|
||||
.direnv
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "python-mpv"]
|
||||
path = lib/python_mpv
|
||||
url = https://github.com/gustaebel/python-mpv.git
|
46
README.md
46
README.md
@ -1,46 +0,0 @@
|
||||
# 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
34
deploy.bash
@ -1,34 +0,0 @@
|
||||
#!/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
14
dist/grzegorz@.service
vendored
@ -1,14 +0,0 @@
|
||||
[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
9
dist/xorg@.service
vendored
@ -1,9 +0,0 @@
|
||||
[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
8
dist/xorg@.socket
vendored
@ -1,8 +0,0 @@
|
||||
[Unit]
|
||||
Description=Socket for xorg at display %i
|
||||
|
||||
[Socket]
|
||||
ListenStream=/tmp/.X11-unix/X%i
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
81
flake.lock
generated
81
flake.lock
generated
@ -1,81 +0,0 @@
|
||||
{
|
||||
"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
142
flake.nix
@ -1,142 +0,0 @@
|
||||
{
|
||||
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 = [];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
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
221
grzegorz/api.py
@ -1,221 +0,0 @@
|
||||
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()
|
@ -1,54 +0,0 @@
|
||||
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
261
grzegorz/mpv.py
@ -1,261 +0,0 @@
|
||||
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()))
|
@ -1,70 +0,0 @@
|
||||
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)
|
@ -1,49 +0,0 @@
|
||||
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]
|
@ -1,34 +0,0 @@
|
||||
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)
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 157 KiB |
@ -1,47 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 2.2 KiB |
22
mpv_control.py
Normal file
22
mpv_control.py
Normal file
@ -0,0 +1,22 @@
|
||||
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
1639
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
||||
[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"
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
mpv==0.1
|
||||
youtube-dl==2016.9.11.1
|
40
server.py
Normal file
40
server.py
Normal file
@ -0,0 +1,40 @@
|
||||
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)
|
Loading…
Reference in New Issue
Block a user