Compare commits
13 Commits
934c4183aa
...
cec
| Author | SHA1 | Date | |
|---|---|---|---|
| c662f1be7a | |||
| 9cbdcee4ab | |||
| 5c23c91867 | |||
| fcfe97ad89 | |||
| 0f5756b064 | |||
| ba04f03d71 | |||
| 58e2b09b8a | |||
| f590e752be | |||
| a90047d14c | |||
| f2c96389ad | |||
| ad231134af | |||
| 03f143cdcb | |||
| b0d16ffd49 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
|||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
config.json
|
config.json
|
||||||
scripts/
|
scripts/
|
||||||
|
!scripts/youtube.py
|
||||||
12
contrib/sway.cfg
Normal file
12
contrib/sway.cfg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Prevent window resizing when we open the player apps
|
||||||
|
workspace_layout stacking
|
||||||
|
|
||||||
|
# Hide the window titlebar
|
||||||
|
default_border none
|
||||||
|
default_floating_border none
|
||||||
|
font pango:monospace 0
|
||||||
|
titlebar_padding 1
|
||||||
|
titlebar_border_thickness 0
|
||||||
|
|
||||||
|
# Start microkodi
|
||||||
|
exec bash <repo path>/start-microkodi.sh
|
||||||
10
microkodi/cec_handler.py
Normal file
10
microkodi/cec_handler.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import cec
|
||||||
|
|
||||||
|
class CECHandler:
|
||||||
|
def __init__(self):
|
||||||
|
cec.init()
|
||||||
|
|
||||||
|
def power_on_if_needed(self):
|
||||||
|
tc = cec.Device(cec.CECDEVICE_TV)
|
||||||
|
if not tc.is_on():
|
||||||
|
tc.power_on()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from microkodi.helpers import recursive_dict_merge
|
from microkodi.helpers import recursive_dict_merge
|
||||||
|
|
||||||
@@ -34,13 +35,12 @@ class Config:
|
|||||||
# URL scheme -> netloc (or '*' for fallback) -> fully-qualified player class
|
# URL scheme -> netloc (or '*' for fallback) -> fully-qualified player class
|
||||||
players: dict[str, dict[str, str]]
|
players: dict[str, dict[str, str]]
|
||||||
|
|
||||||
card: str | None
|
# The entire configuration file for use in user scripts
|
||||||
connector: str | None
|
options: dict[str, Any]
|
||||||
|
|
||||||
@property
|
|
||||||
def watch_connector(self) -> bool:
|
|
||||||
return self.card is not None and self.connector is not None
|
|
||||||
|
|
||||||
|
# Enables the use of CEC
|
||||||
|
cec: bool
|
||||||
|
|
||||||
def load_config(config_path: Path | None) -> Config:
|
def load_config(config_path: Path | None) -> Config:
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
config_data = {}
|
config_data = {}
|
||||||
@@ -66,6 +66,6 @@ def load_config(config_path: Path | None) -> Config:
|
|||||||
},
|
},
|
||||||
config_data.get("players", {}),
|
config_data.get("players", {}),
|
||||||
),
|
),
|
||||||
card=config_data.get("card"),
|
options=config_data.get("options", {}),
|
||||||
connector=config_data.get("connector"),
|
cec=config_data.get("cec", False),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TypeVar, Callable
|
from typing import TypeVar, ParamSpec, Callable
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
T = TypeVar("T")
|
||||||
|
def after(func: Callable[[], None]) -> Callable[P, T]:
|
||||||
|
"""Runs @func after the decorated function exits."""
|
||||||
|
def decorator(f: Callable[P, T]) -> Callable[P, T]:
|
||||||
|
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
|
ret = f(*args, **kwargs)
|
||||||
|
func()
|
||||||
|
return ret
|
||||||
|
return inner
|
||||||
|
return decorator
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class KodiTime:
|
class KodiTime:
|
||||||
@@ -7,6 +20,9 @@ class KodiTime:
|
|||||||
minutes: int
|
minutes: int
|
||||||
seconds: int
|
seconds: int
|
||||||
|
|
||||||
|
def to_seconds(self) -> int:
|
||||||
|
return self.hours * 3600 + self.minutes * 59 + self.seconds
|
||||||
|
|
||||||
def seconds_to_kodi_format(seconds: int) -> KodiTime:
|
def seconds_to_kodi_format(seconds: int) -> KodiTime:
|
||||||
"""Convert seconds into hours, minutes, and seconds as Kodi wants that."""
|
"""Convert seconds into hours, minutes, and seconds as Kodi wants that."""
|
||||||
hours = seconds // 3600
|
hours = seconds // 3600
|
||||||
@@ -20,7 +36,6 @@ def seconds_to_kodi_format(seconds: int) -> KodiTime:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
def find(l: list[T], pred: Callable[[T], bool]) -> T | None:
|
def find(l: list[T], pred: Callable[[T], bool]) -> T | None:
|
||||||
for i in l:
|
for i in l:
|
||||||
if pred(i):
|
if pred(i):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import logging
|
import logging
|
||||||
import base64
|
import base64
|
||||||
@@ -9,7 +10,7 @@ from http.server import BaseHTTPRequestHandler
|
|||||||
|
|
||||||
from microkodi.repository import I
|
from microkodi.repository import I
|
||||||
from microkodi.programs import Program, PlayerInfo
|
from microkodi.programs import Program, PlayerInfo
|
||||||
from microkodi.helpers import seconds_to_kodi_format
|
from microkodi.helpers import KodiTime, seconds_to_kodi_format, after
|
||||||
|
|
||||||
def jsonrpc_response(id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
def jsonrpc_response(id: int, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -99,9 +100,25 @@ class PlayerRpcObject(JsonRpcObject):
|
|||||||
return self.open(params)
|
return self.open(params)
|
||||||
elif method == "PlayPause":
|
elif method == "PlayPause":
|
||||||
return self.play_pause(params)
|
return self.play_pause(params)
|
||||||
|
elif method == "Seek":
|
||||||
|
return self.seek(params)
|
||||||
else:
|
else:
|
||||||
return "Unknown method"
|
return "Unknown method"
|
||||||
|
|
||||||
|
def seek(self, params: dict[str, Any]) -> Any:
|
||||||
|
if not self.is_active:
|
||||||
|
self.logger.warn("Trying to seek player that is not active")
|
||||||
|
return "ERR"
|
||||||
|
|
||||||
|
time_raw = params["value"]["time"]
|
||||||
|
kodi_time = KodiTime(
|
||||||
|
hours=time_raw["hours"],
|
||||||
|
minutes=time_raw["minutes"],
|
||||||
|
seconds=time_raw["seconds"],
|
||||||
|
)
|
||||||
|
self._active_program.seek(kodi_time)
|
||||||
|
return "Ok"
|
||||||
|
|
||||||
def get_active_players(self, params: dict[str, Any]) -> Any:
|
def get_active_players(self, params: dict[str, Any]) -> Any:
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
return []
|
return []
|
||||||
@@ -132,7 +149,14 @@ class PlayerRpcObject(JsonRpcObject):
|
|||||||
self._active_program.stop("playerrpcobject.stop")
|
self._active_program.stop("playerrpcobject.stop")
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
@after(lambda: I.get("DataBridge").set_loading(False))
|
||||||
def open(self, params: dict[str, Any]) -> Any:
|
def open(self, params: dict[str, Any]) -> Any:
|
||||||
|
# Turn on the TV
|
||||||
|
config: Config = I.get("Config")
|
||||||
|
if config.cec:
|
||||||
|
I.get("CECHandler").power_on_if_needed()
|
||||||
|
|
||||||
|
I.get("DataBridge").set_loading(True)
|
||||||
url = urlparse(params["item"]["file"])
|
url = urlparse(params["item"]["file"])
|
||||||
|
|
||||||
# Handle plugins
|
# Handle plugins
|
||||||
@@ -142,9 +166,9 @@ class PlayerRpcObject(JsonRpcObject):
|
|||||||
url = urlparse(url.query)
|
url = urlparse(url.query)
|
||||||
|
|
||||||
# Find out what player class to use
|
# Find out what player class to use
|
||||||
config: Config = I.get("Config")
|
|
||||||
scheme_configuration = config.players.get(url.scheme)
|
scheme_configuration = config.players.get(url.scheme)
|
||||||
if scheme_configuration is None:
|
if scheme_configuration is None:
|
||||||
|
I.get("DataBridge").notification.emit(f"No player available for {url.scheme}")
|
||||||
self.logger.warn("Client requested unknown scheme: '%s'", url.scheme)
|
self.logger.warn("Client requested unknown scheme: '%s'", url.scheme)
|
||||||
return {
|
return {
|
||||||
"error": "invalid protocol"
|
"error": "invalid protocol"
|
||||||
@@ -153,6 +177,7 @@ class PlayerRpcObject(JsonRpcObject):
|
|||||||
if player_class_name is None:
|
if player_class_name is None:
|
||||||
player_class_name = scheme_configuration.get("*")
|
player_class_name = scheme_configuration.get("*")
|
||||||
if player_class_name is None:
|
if player_class_name is None:
|
||||||
|
I.get("DataBridge").notification.emit(f"No player available for {url.netloc}")
|
||||||
self.logger.warn("No player was picked for url '%s'", url)
|
self.logger.warn("No player was picked for url '%s'", url)
|
||||||
return {
|
return {
|
||||||
"error": "invalid protocol"
|
"error": "invalid protocol"
|
||||||
@@ -171,12 +196,12 @@ class PlayerRpcObject(JsonRpcObject):
|
|||||||
program_cls = getattr(sys.modules[module_name], class_name, None)
|
program_cls = getattr(sys.modules[module_name], class_name, None)
|
||||||
|
|
||||||
if program_cls is None:
|
if program_cls is None:
|
||||||
|
I.get("DataBridge").notification.emit("Could not start player")
|
||||||
self.logger.warn("Class %s not found in module %s", class_name, module_name)
|
self.logger.warn("Class %s not found in module %s", class_name, module_name)
|
||||||
return {
|
return {
|
||||||
"error": "invalid protocol"
|
"error": "invalid protocol"
|
||||||
}
|
}
|
||||||
|
|
||||||
I.get("DataBridge").set_loading(True)
|
|
||||||
if self._active_program is not None:
|
if self._active_program is not None:
|
||||||
if isinstance(self._active_program, program_cls):
|
if isinstance(self._active_program, program_cls):
|
||||||
self._active_program.stop("playerrpcobject.open")
|
self._active_program.stop("playerrpcobject.open")
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from PySide6.QtQml import QQmlApplicationEngine
|
|||||||
from microkodi.jsonrpc import JsonRpcHandler, GlobalMethodHandler
|
from microkodi.jsonrpc import JsonRpcHandler, GlobalMethodHandler
|
||||||
from microkodi.ui.bridge import DataBridge
|
from microkodi.ui.bridge import DataBridge
|
||||||
from microkodi.config import Config, load_config
|
from microkodi.config import Config, load_config
|
||||||
|
from microkodi.cec_handler import CECHandler
|
||||||
from microkodi.repository import I
|
from microkodi.repository import I
|
||||||
from microkodi.udev import is_display_connected, block_until_display_connected
|
|
||||||
|
|
||||||
|
|
||||||
def run_kodi_server():
|
def run_kodi_server():
|
||||||
@@ -24,6 +24,11 @@ def run_kodi_server():
|
|||||||
method_handler = GlobalMethodHandler()
|
method_handler = GlobalMethodHandler()
|
||||||
I.register("GlobalMethodHandler", method_handler)
|
I.register("GlobalMethodHandler", method_handler)
|
||||||
|
|
||||||
|
# Setup CEC
|
||||||
|
if config.cec:
|
||||||
|
I.register("CECHandler", CECHandler())
|
||||||
|
logger.info("Enabling CEC support")
|
||||||
|
|
||||||
# Load extra plugins
|
# Load extra plugins
|
||||||
if config.scripts:
|
if config.scripts:
|
||||||
logger.info("Loading scripts...")
|
logger.info("Loading scripts...")
|
||||||
@@ -92,25 +97,6 @@ if __name__ == "__main__":
|
|||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
engine.rootObjects()[0].setProperty("bridge", bridge)
|
engine.rootObjects()[0].setProperty("bridge", bridge)
|
||||||
|
exit_code = app.exec()
|
||||||
if config.watch_connector:
|
|
||||||
logger.info("Will be watching display if it's gone")
|
|
||||||
|
|
||||||
exit_code = 0
|
|
||||||
while True:
|
|
||||||
exit_code = app.exec()
|
|
||||||
|
|
||||||
if not config.watch_connector:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Exit if the display is still connected
|
|
||||||
if is_display_connected(config.card, config.connector):
|
|
||||||
break
|
|
||||||
|
|
||||||
logger.info("Display is gone. Waiting until it's back")
|
|
||||||
block_until_display_connected(config.card, config.connector)
|
|
||||||
logger.info("Display is back. Waiting 500ms...")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
del engine
|
del engine
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
@@ -2,6 +2,8 @@ from dataclasses import dataclass
|
|||||||
from urllib.parse import ParseResult
|
from urllib.parse import ParseResult
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from microkodi.helpers import KodiTime
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlayerInfo:
|
class PlayerInfo:
|
||||||
runtime: int
|
runtime: int
|
||||||
@@ -38,3 +40,6 @@ class Program:
|
|||||||
|
|
||||||
def get_player_info(self) -> PlayerInfo:
|
def get_player_info(self) -> PlayerInfo:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def seek(self, timestamp: KodiTime):
|
||||||
|
raise NotImplementedError()
|
||||||
@@ -108,16 +108,18 @@ class MpvProgram(Program):
|
|||||||
|
|
||||||
def _when_mpv_exit(self):
|
def _when_mpv_exit(self):
|
||||||
self.logger.info("MPV has exited")
|
self.logger.info("MPV has exited")
|
||||||
self._process = None
|
|
||||||
I.get("DataBridge").set_loading(False)
|
I.get("DataBridge").set_loading(False)
|
||||||
|
|
||||||
|
if self._process.returncode != 0:
|
||||||
|
I.get("DataBridge").notification.emit("mpv exited with an error")
|
||||||
|
self._process = None
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
self.__mpv_command(["set_property", "pause", True])
|
self.__mpv_command(["set_property", "pause", True])
|
||||||
|
|
||||||
def stop(self, src):
|
def stop(self, src):
|
||||||
if self.is_active():
|
if self.is_active():
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
self._process = None
|
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
self.stop("mpvplayer.exit")
|
self.stop("mpvplayer.exit")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
from typing import Callable
|
from typing import Any, Callable
|
||||||
from urllib.parse import ParseResult, urlunparse
|
from urllib.parse import ParseResult, urlunparse
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
@@ -11,6 +11,7 @@ from microkodi.process import nonblocking_run
|
|||||||
from microkodi.config import Config
|
from microkodi.config import Config
|
||||||
from microkodi.programs import PlayerInfo, Program
|
from microkodi.programs import PlayerInfo, Program
|
||||||
from microkodi.repository import I
|
from microkodi.repository import I
|
||||||
|
from microkodi.helpers import KodiTime
|
||||||
|
|
||||||
EVENT_PLAYER_EXIT = "post-player-exit"
|
EVENT_PLAYER_EXIT = "post-player-exit"
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ class VlcProgram(Program):
|
|||||||
return self._process is not None and self._process.returncode is None
|
return self._process is not None and self._process.returncode is None
|
||||||
|
|
||||||
def is_playing(self) -> bool:
|
def is_playing(self) -> bool:
|
||||||
return self.get_player_info().is_playing
|
return self.get_player_info().playing
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
self.__vlc_command("pl_pause")
|
self.__vlc_command("pl_pause")
|
||||||
@@ -122,6 +123,7 @@ class VlcProgram(Program):
|
|||||||
"--http-port=9090",
|
"--http-port=9090",
|
||||||
f"--http-password={self._vlc_password}",
|
f"--http-password={self._vlc_password}",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
|
"--play-and-exit",
|
||||||
*extra_args,
|
*extra_args,
|
||||||
final_url,
|
final_url,
|
||||||
]
|
]
|
||||||
@@ -130,15 +132,22 @@ class VlcProgram(Program):
|
|||||||
|
|
||||||
def _when_vlc_exit(self):
|
def _when_vlc_exit(self):
|
||||||
self.logger.info("vlc has exited")
|
self.logger.info("vlc has exited")
|
||||||
self._process = None
|
|
||||||
I.get("VlcConfig").run_event_listeners(EVENT_PLAYER_EXIT)
|
I.get("VlcConfig").run_event_listeners(EVENT_PLAYER_EXIT)
|
||||||
I.get("DataBridge").set_loading(False)
|
I.get("DataBridge").set_loading(False)
|
||||||
|
|
||||||
def __vlc_command(self, command: str) -> str | None:
|
if self._process.returncode != 0:
|
||||||
|
I.get("DataBridge").notification.emit("VLC exited with an error")
|
||||||
|
self._process = None
|
||||||
|
|
||||||
|
def __vlc_command(self, command: str, params: dict[str, Any] | None = None) -> str | None:
|
||||||
try:
|
try:
|
||||||
req = requests.get(
|
req = requests.get(
|
||||||
f"http://127.0.0.1:9090/requests/status.xml?command={command}",
|
f"http://127.0.0.1:9090/requests/status.xml",
|
||||||
auth=("", self._vlc_password),
|
auth=("", self._vlc_password),
|
||||||
|
params={
|
||||||
|
"command": command,
|
||||||
|
**(params or {}),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return req.text
|
return req.text
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -151,7 +160,14 @@ class VlcProgram(Program):
|
|||||||
def stop(self, src):
|
def stop(self, src):
|
||||||
if self.is_active():
|
if self.is_active():
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
self._process = None
|
|
||||||
|
def seek(self, timestamp: KodiTime):
|
||||||
|
self.__vlc_command(
|
||||||
|
"seek",
|
||||||
|
{
|
||||||
|
"val": timestamp.to_seconds(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
self.stop("mpvplayer.exit")
|
self.stop("mpvplayer.exit")
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ Window {
|
|||||||
function onIsLoading(loading) {
|
function onIsLoading(loading) {
|
||||||
isLoading = loading
|
isLoading = loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onNotification(text) {
|
||||||
|
notificationModel.append({"message": text})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@@ -67,4 +71,47 @@ Window {
|
|||||||
anchors.rightMargin: 20
|
anchors.rightMargin: 20
|
||||||
font.pixelSize: window.height * 0.1
|
font.pixelSize: window.height * 0.1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: notificationModel
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 8 * 1000
|
||||||
|
running: notificationModel.count > 0
|
||||||
|
repeat: true
|
||||||
|
onTriggered: notificationModel.remove(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: notificationDelegate
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: "#37474F"
|
||||||
|
width: 400
|
||||||
|
height: 100
|
||||||
|
radius: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: message
|
||||||
|
color: "white"
|
||||||
|
font.pixelSize: 20
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
anchors.right: wallpaperImage.right
|
||||||
|
anchors.top: wallpaperImage.top
|
||||||
|
anchors.topMargin: 20 * 2 + window.height * 0.1
|
||||||
|
anchors.rightMargin: 20
|
||||||
|
width: 400
|
||||||
|
height: window.height * 0.9 - 20 * 2
|
||||||
|
|
||||||
|
spacing: 50
|
||||||
|
model: notificationModel
|
||||||
|
delegate: notificationDelegate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import pyudev
|
|
||||||
|
|
||||||
def is_display_connected(card: str, connector: str) -> bool:
|
|
||||||
logger = logging.getLogger("udev")
|
|
||||||
status_file = f"/sys/class/drm/{card}-{connector}/status"
|
|
||||||
logger.debug("Reading file %s", status_file)
|
|
||||||
with open(status_file, "r") as f:
|
|
||||||
result = f.read().strip()
|
|
||||||
logger.debug("Result: '%s'", result)
|
|
||||||
return result == "connected"
|
|
||||||
|
|
||||||
def block_until_display_connected(card: str, connector: str):
|
|
||||||
ctx = pyudev.Context()
|
|
||||||
monitor = pyudev.Monitor.from_netlink(ctx)
|
|
||||||
monitor.filter_by("drm")
|
|
||||||
for device in iter(monitor.poll, None):
|
|
||||||
if not "DEVNAME" in device:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if device.get("DEVNAME") != f"/dev/dri/{card}":
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_display_connected(card, connector):
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
@@ -8,6 +8,8 @@ class DataBridge(QObject):
|
|||||||
# Indicates whether we're currently loading something or not
|
# Indicates whether we're currently loading something or not
|
||||||
isLoading = Signal(bool, arguments=["loading"])
|
isLoading = Signal(bool, arguments=["loading"])
|
||||||
|
|
||||||
|
notification = Signal(str, arguments=["text"])
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ dependencies = [
|
|||||||
"pyside6",
|
"pyside6",
|
||||||
"requests",
|
"requests",
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
"pyudev"
|
"cec"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tools.build]
|
[tools.build]
|
||||||
|
|||||||
56
scripts/youtube.py
Normal file
56
scripts/youtube.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from urllib.parse import ParseResult, urlunparse, urlparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from microkodi.repository import I
|
||||||
|
from microkodi.config import Config
|
||||||
|
from microkodi.helpers import recursive_dict_merge
|
||||||
|
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"format": "bestvideo[width<=1920]+bestaudio",
|
||||||
|
"ytdlp_options": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def youtube_url_transformer(url: ParseResult) -> tuple[list[str], str]:
|
||||||
|
logger = logging.getLogger("Youtube")
|
||||||
|
youtube_config = I.get("YoutubeConfig")
|
||||||
|
opts = {
|
||||||
|
"format": youtube_config["format"],
|
||||||
|
**youtube_config["ytdlp_options"],
|
||||||
|
}
|
||||||
|
logger.debug("Using config for yt-dlp: %s", opts)
|
||||||
|
|
||||||
|
with yt_dlp.YoutubeDL(opts) as ytdl:
|
||||||
|
info = ytdl.extract_info(urlunparse(url), download=False)
|
||||||
|
|
||||||
|
user_agent = None
|
||||||
|
audio_url = None
|
||||||
|
video_url = None
|
||||||
|
for format in info["requested_formats"]:
|
||||||
|
if format["width"] is None:
|
||||||
|
audio_url = format["url"]
|
||||||
|
else:
|
||||||
|
user_agent = format["http_headers"]["User-Agent"]
|
||||||
|
video_url = format["url"]
|
||||||
|
|
||||||
|
args = [
|
||||||
|
f'--input-slave={audio_url}',
|
||||||
|
] if audio_url else None
|
||||||
|
return args, urlparse(video_url)
|
||||||
|
|
||||||
|
def init():
|
||||||
|
# Create the config
|
||||||
|
config: Config = I.get("Config")
|
||||||
|
youtube_config = recursive_dict_merge(
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
config.options.get(
|
||||||
|
"me.polynom.microkodi.youtube",
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
I.register("YoutubeConfig", youtube_config)
|
||||||
|
|
||||||
|
# Register the transformers
|
||||||
|
I.get("VlcConfig").register_domain_transformer("youtu.be", youtube_url_transformer)
|
||||||
|
I.get("VlcConfig").register_domain_transformer("www.youtube.com", youtube_url_transformer)
|
||||||
4
start-microkodi.sh
Executable file
4
start-microkodi.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd `dirname $0`
|
||||||
|
source .venv/bin/activate
|
||||||
|
python3 microkodi/main.py -c ./config.json $@
|
||||||
Reference in New Issue
Block a user