aurclips 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aurclips-0.2.0 → aurclips-0.2.1}/PKG-INFO +1 -1
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/__init__.py +1 -1
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/marks.py +35 -16
- aurclips-0.2.1/aurclips/player.py +190 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/grabar-en-beats.md +8 -4
- aurclips-0.2.0/aurclips/player.py +0 -97
- {aurclips-0.2.0 → aurclips-0.2.1}/.gitignore +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/CONTEXT.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/LICENSE +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/README.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/__main__.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/assets/config.default.yaml +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/assets/fonts/Anton-Regular.ttf +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/assets/fonts/OFL.txt +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/clipper.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/config.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/facecrop.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/heuristics.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/ingest.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/notify.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/render.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/runner.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/safety.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/select_clips.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/state.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/stats.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/subtitles.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/titles.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/transcribe.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/aurclips/upload.py +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/config.yaml +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0001-extremos-apretados-centro-simple.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0002-recortador-en-la-puerta-publicador-dentro.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0003-rutas-checkout-y-usuario.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/config.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/pipeline.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/selection.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/docs/upload-youtube.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/packaging/README.md +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/packaging/crontab.example +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/packaging/launchd/com.aurclips.daily.plist +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/packaging/systemd/aurclips.service +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/packaging/systemd/aurclips.timer +0 -0
- {aurclips-0.2.0 → aurclips-0.2.1}/pyproject.toml +0 -0
|
@@ -421,33 +421,52 @@ def review_session(video_path: str | Path) -> Path:
|
|
|
421
421
|
print(f"Repasando: {video.name}")
|
|
422
422
|
if existing:
|
|
423
423
|
print(f" ({len(existing)} marca(s) preexistentes; se conservan)")
|
|
424
|
-
print(" Enter -> marca el momento que está sonando
|
|
425
|
-
print("
|
|
424
|
+
print(" Enter -> marca el momento que está sonando — EN EL REPRODUCTOR")
|
|
425
|
+
print(" (o en este terminal); pausar también vale")
|
|
426
|
+
print(" u -> deshacer la última marca (también en el reproductor)")
|
|
427
|
+
print(" volumen en mpv: 9 baja · 0 sube · m mutea")
|
|
426
428
|
print(" Ctrl+C o cerrar el reproductor -> terminar y guardar\n")
|
|
427
429
|
|
|
428
430
|
player = MpvPlayer(mpv, video)
|
|
429
431
|
lines = _stdin_queue()
|
|
432
|
+
|
|
433
|
+
def do_mark():
|
|
434
|
+
t = player.playback_time()
|
|
435
|
+
if t is None:
|
|
436
|
+
print(" no pude leer la posición (¿mpv sigue abriendo?)")
|
|
437
|
+
elif session.mark(t) is None:
|
|
438
|
+
print(f" ya hay una marca en {_mmss(t)}; no se duplica")
|
|
439
|
+
player.show_message(f"ya marcado ({_mmss(t)})")
|
|
440
|
+
else:
|
|
441
|
+
n = len(session.all_marks())
|
|
442
|
+
print(f" marca {n:>2} en {_mmss(t)}")
|
|
443
|
+
player.show_message(f"marca {n} - {_mmss(t)}")
|
|
444
|
+
|
|
445
|
+
def do_undo():
|
|
446
|
+
undone = session.undo()
|
|
447
|
+
if undone is None:
|
|
448
|
+
print(" nada que deshacer en esta sesión")
|
|
449
|
+
player.show_message("nada que deshacer")
|
|
450
|
+
else:
|
|
451
|
+
print(f" deshecha la marca de {_mmss(undone)}")
|
|
452
|
+
player.show_message(f"deshecha la marca de {_mmss(undone)}")
|
|
453
|
+
|
|
430
454
|
try:
|
|
431
455
|
while player.alive():
|
|
456
|
+
# dos fuentes con las mismas teclas: el reproductor y el terminal
|
|
457
|
+
event = player.poll_event(timeout=0.15)
|
|
458
|
+
if event == "mark":
|
|
459
|
+
do_mark()
|
|
460
|
+
elif event == "undo":
|
|
461
|
+
do_undo()
|
|
432
462
|
try:
|
|
433
|
-
|
|
463
|
+
command = lines.get_nowait().strip().lower()
|
|
434
464
|
except queue.Empty:
|
|
435
465
|
continue
|
|
436
|
-
command = line.strip().lower()
|
|
437
466
|
if command == "u":
|
|
438
|
-
|
|
439
|
-
if undone is None:
|
|
440
|
-
print(" nada que deshacer en esta sesión")
|
|
441
|
-
else:
|
|
442
|
-
print(f" deshecha la marca de {_mmss(undone)}")
|
|
467
|
+
do_undo()
|
|
443
468
|
elif command == "":
|
|
444
|
-
|
|
445
|
-
if t is None:
|
|
446
|
-
print(" no pude leer la posición (¿mpv sigue abriendo?)")
|
|
447
|
-
elif session.mark(t) is None:
|
|
448
|
-
print(f" ya hay una marca en {_mmss(t)}; no se duplica")
|
|
449
|
-
else:
|
|
450
|
-
print(f" marca {len(session.all_marks()):>2} en {_mmss(t)}")
|
|
469
|
+
do_mark()
|
|
451
470
|
# cualquier otra entrada se ignora sin ruido
|
|
452
471
|
except KeyboardInterrupt:
|
|
453
472
|
print()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Transporte con mpv para el repaso: lanzar, preguntar la posición, cerrar.
|
|
2
|
+
|
|
3
|
+
Delgado a propósito y sin tests automáticos — el mismo trato que ffmpeg: aquí
|
|
4
|
+
no hay lógica, solo el cable. La lógica del repaso vive en marks.MarkingSession
|
|
5
|
+
y se prueba sin nada de esto.
|
|
6
|
+
|
|
7
|
+
mpv es el único reproductor multiplataforma con un IPC limpio para preguntar
|
|
8
|
+
``playback-time``: un named pipe en Windows, un unix socket en POSIX, JSON por
|
|
9
|
+
línea en ambos. Es dependencia opcional solo de este modo.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import queue
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import tempfile
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from .config import ROOT, _exe_suffix
|
|
25
|
+
|
|
26
|
+
# Teclas que marcan DENTRO del reproductor. La primera prueba real lo dejó
|
|
27
|
+
# claro: quien repasa tiene las manos y los ojos en el video, no en el
|
|
28
|
+
# terminal — los Enter iban a la ventana de mpv y se perdían. Se le piden a
|
|
29
|
+
# mpv por IPC (comando keybind -> script-message) y llegan como eventos
|
|
30
|
+
# client-message; las mismas teclas que en el terminal.
|
|
31
|
+
KEY_BINDINGS = (
|
|
32
|
+
("ENTER", "aurclips-mark"),
|
|
33
|
+
("u", "aurclips-undo"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_mpv() -> str:
|
|
38
|
+
"""Ruta de mpv, o un error de una línea con la instalación por SO.
|
|
39
|
+
|
|
40
|
+
Como los demás binarios del repo: primero el empaquetado en tools/ (la
|
|
41
|
+
convención de Windows, donde también viven ffmpeg y deno), luego el PATH.
|
|
42
|
+
"""
|
|
43
|
+
bundled = ROOT / "tools" / "mpv" / f"mpv{_exe_suffix()}"
|
|
44
|
+
if bundled.exists():
|
|
45
|
+
return str(bundled)
|
|
46
|
+
found = shutil.which("mpv")
|
|
47
|
+
if found:
|
|
48
|
+
return found
|
|
49
|
+
raise FileNotFoundError(
|
|
50
|
+
"No se encontró mpv (el reproductor del repaso). Instálalo y agrégalo "
|
|
51
|
+
"al PATH (macOS: brew install mpv · Linux: apt install mpv · "
|
|
52
|
+
"Windows: winget install mpv), o deja el portable en tools/mpv."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MpvPlayer:
|
|
57
|
+
"""Una instancia de mpv reproduciendo un video, con su socket de IPC."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, mpv_path: str, video_path: str | Path):
|
|
60
|
+
if os.name == "nt":
|
|
61
|
+
self._ipc = rf"\\.\pipe\aurclips-mpv-{os.getpid()}"
|
|
62
|
+
else:
|
|
63
|
+
self._ipc = str(Path(tempfile.gettempdir()) / f"aurclips-mpv-{os.getpid()}.sock")
|
|
64
|
+
# stdin=DEVNULL: el terminal es de la sesión de marcado; sin esto mpv
|
|
65
|
+
# heredaría la consola y competiría con el hilo lector por las teclas
|
|
66
|
+
self._proc = subprocess.Popen(
|
|
67
|
+
[mpv_path, f"--input-ipc-server={self._ipc}", "--", str(video_path)],
|
|
68
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
69
|
+
stderr=subprocess.DEVNULL,
|
|
70
|
+
)
|
|
71
|
+
self._events: queue.Queue[str] = queue.Queue()
|
|
72
|
+
threading.Thread(target=self._event_pump, daemon=True).start()
|
|
73
|
+
|
|
74
|
+
def poll_event(self, timeout: float) -> str | None:
|
|
75
|
+
"""'mark' o 'undo' si el usuario pulsó la tecla en el reproductor."""
|
|
76
|
+
try:
|
|
77
|
+
return self._events.get(timeout=timeout)
|
|
78
|
+
except queue.Empty:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def show_message(self, text: str) -> None:
|
|
82
|
+
"""OSD en el reproductor: el eco de la marca, donde están los ojos."""
|
|
83
|
+
try:
|
|
84
|
+
self._roundtrip(json.dumps({"command": ["show-text", text, 1500]}) + "\n")
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def _event_pump(self) -> None:
|
|
89
|
+
"""Hilo de eventos: registra las teclas en mpv y escucha sus pulsaciones.
|
|
90
|
+
|
|
91
|
+
Conexión propia y persistente (el resto de peticiones va por conexiones
|
|
92
|
+
cortas aparte, así no se entrelazan). Muere sola cuando mpv se cierra.
|
|
93
|
+
"""
|
|
94
|
+
conn = self._connect_for_events()
|
|
95
|
+
if conn is None:
|
|
96
|
+
return
|
|
97
|
+
write, read, close = conn
|
|
98
|
+
try:
|
|
99
|
+
for key, message in KEY_BINDINGS:
|
|
100
|
+
write((json.dumps({"command": ["keybind", key,
|
|
101
|
+
f"script-message {message}"]}) + "\n").encode("utf-8"))
|
|
102
|
+
buffer = b""
|
|
103
|
+
while True:
|
|
104
|
+
data = read()
|
|
105
|
+
if not data: # mpv cerró la conexión
|
|
106
|
+
return
|
|
107
|
+
buffer += data
|
|
108
|
+
while b"\n" in buffer:
|
|
109
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
110
|
+
try:
|
|
111
|
+
payload = json.loads(line)
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
continue
|
|
114
|
+
if payload.get("event") == "client-message":
|
|
115
|
+
args = payload.get("args") or []
|
|
116
|
+
if args and str(args[0]).startswith("aurclips-"):
|
|
117
|
+
self._events.put(str(args[0]).removeprefix("aurclips-"))
|
|
118
|
+
except OSError:
|
|
119
|
+
return
|
|
120
|
+
finally:
|
|
121
|
+
try:
|
|
122
|
+
close()
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def _connect_for_events(self):
|
|
127
|
+
"""(write, read, close) sobre el IPC, esperando a que mpv lo cree."""
|
|
128
|
+
for _ in range(40): # mpv tarda un instante en crear el socket/pipe
|
|
129
|
+
if not self.alive():
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
if os.name == "nt":
|
|
133
|
+
pipe = open(self._ipc, "r+b", buffering=0)
|
|
134
|
+
return pipe.write, (lambda: pipe.read(4096)), pipe.close
|
|
135
|
+
import socket
|
|
136
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
137
|
+
sock.connect(self._ipc)
|
|
138
|
+
return sock.sendall, (lambda: sock.recv(4096)), sock.close
|
|
139
|
+
except OSError:
|
|
140
|
+
time.sleep(0.25)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def alive(self) -> bool:
|
|
144
|
+
return self._proc.poll() is None
|
|
145
|
+
|
|
146
|
+
def playback_time(self) -> float | None:
|
|
147
|
+
"""Posición de reproducción en segundos; None si no se pudo leer."""
|
|
148
|
+
request = json.dumps({"command": ["get_property", "playback-time"]}) + "\n"
|
|
149
|
+
try:
|
|
150
|
+
for line in self._roundtrip(request):
|
|
151
|
+
try:
|
|
152
|
+
payload = json.loads(line)
|
|
153
|
+
except json.JSONDecodeError:
|
|
154
|
+
continue
|
|
155
|
+
if "error" in payload: # respuesta a la petición (no un evento)
|
|
156
|
+
if payload["error"] == "success" and isinstance(
|
|
157
|
+
payload.get("data"), (int, float)):
|
|
158
|
+
return float(payload["data"])
|
|
159
|
+
return None
|
|
160
|
+
except OSError:
|
|
161
|
+
return None
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def _roundtrip(self, request: str) -> list[str]:
|
|
165
|
+
"""Envía una petición y devuelve las líneas de respuesta."""
|
|
166
|
+
if os.name == "nt":
|
|
167
|
+
# el pipe lo crea mpv; abrir/escribir/leer por petición es lo robusto
|
|
168
|
+
with open(self._ipc, "r+b", buffering=0) as pipe:
|
|
169
|
+
pipe.write(request.encode("utf-8"))
|
|
170
|
+
return pipe.read(4096).decode("utf-8", "replace").splitlines()
|
|
171
|
+
import socket
|
|
172
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
173
|
+
sock.settimeout(1.0)
|
|
174
|
+
sock.connect(self._ipc)
|
|
175
|
+
sock.sendall(request.encode("utf-8"))
|
|
176
|
+
chunks = b""
|
|
177
|
+
while b"\n" not in chunks:
|
|
178
|
+
data = sock.recv(4096)
|
|
179
|
+
if not data:
|
|
180
|
+
break
|
|
181
|
+
chunks += data
|
|
182
|
+
return chunks.decode("utf-8", "replace").splitlines()
|
|
183
|
+
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
if self.alive():
|
|
186
|
+
self._proc.terminate()
|
|
187
|
+
try:
|
|
188
|
+
self._proc.wait(timeout=3)
|
|
189
|
+
except subprocess.TimeoutExpired:
|
|
190
|
+
self._proc.kill()
|
|
@@ -91,13 +91,16 @@ material descargado de canales directamente no es tuyo. Para eso está el
|
|
|
91
91
|
aurclips mark mi-grabacion.mp4
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Se abre el video en el reproductor y
|
|
94
|
+
Se abre el video en el reproductor y marcas **sin salir de él**:
|
|
95
95
|
|
|
96
|
-
- **Enter**
|
|
97
|
-
|
|
96
|
+
- **Enter** (en el reproductor o en el terminal) marca el momento que está
|
|
97
|
+
sonando, con confirmación en la propia pantalla del video. Pausar también
|
|
98
|
+
vale: la marca cae donde dejaste el cursor, con la precisión que quieras.
|
|
98
99
|
- **u** deshace la última marca de esta sesión (las que ya existían no se
|
|
99
100
|
tocan).
|
|
100
101
|
- **Ctrl+C** o cerrar el reproductor termina y guarda.
|
|
102
|
+
- Controles útiles de mpv: espacio pausa, flechas buscan, `[` y `]` cambian la
|
|
103
|
+
velocidad, `9`/`0` bajan/suben el volumen, `m` mutea.
|
|
101
104
|
|
|
102
105
|
Las marcas van al mismo `<video>.marks.txt` de siempre, junto al video, y las
|
|
103
106
|
que ya hubiera —de la voz, del hotkey o de un repaso anterior— se conservan:
|
|
@@ -116,7 +119,8 @@ aurclips clip partida.mp4 # los momentos marcados son los recortes
|
|
|
116
119
|
|
|
117
120
|
El repaso necesita **[mpv](https://mpv.io)** (solo este modo lo usa): `winget
|
|
118
121
|
install mpv` en Windows, `brew install mpv` en macOS, `apt install mpv` en
|
|
119
|
-
Linux.
|
|
122
|
+
Linux. En Windows también vale dejar el [build portable](https://mpv.io/installation/)
|
|
123
|
+
descomprimido en `tools/mpv`, junto a los demás binarios del repo.
|
|
120
124
|
|
|
121
125
|
## Qué NO tienes que hacer
|
|
122
126
|
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"""Transporte con mpv para el repaso: lanzar, preguntar la posición, cerrar.
|
|
2
|
-
|
|
3
|
-
Delgado a propósito y sin tests automáticos — el mismo trato que ffmpeg: aquí
|
|
4
|
-
no hay lógica, solo el cable. La lógica del repaso vive en marks.MarkingSession
|
|
5
|
-
y se prueba sin nada de esto.
|
|
6
|
-
|
|
7
|
-
mpv es el único reproductor multiplataforma con un IPC limpio para preguntar
|
|
8
|
-
``playback-time``: un named pipe en Windows, un unix socket en POSIX, JSON por
|
|
9
|
-
línea en ambos. Es dependencia opcional solo de este modo.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
import os
|
|
16
|
-
import shutil
|
|
17
|
-
import subprocess
|
|
18
|
-
import tempfile
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def find_mpv() -> str:
|
|
23
|
-
"""Ruta de mpv, o un error de una línea con la instalación por SO."""
|
|
24
|
-
found = shutil.which("mpv")
|
|
25
|
-
if found:
|
|
26
|
-
return found
|
|
27
|
-
raise FileNotFoundError(
|
|
28
|
-
"No se encontró mpv (el reproductor del repaso). Instálalo y agrégalo "
|
|
29
|
-
"al PATH (macOS: brew install mpv · Linux: apt install mpv · "
|
|
30
|
-
"Windows: winget install mpv)."
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class MpvPlayer:
|
|
35
|
-
"""Una instancia de mpv reproduciendo un video, con su socket de IPC."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, mpv_path: str, video_path: str | Path):
|
|
38
|
-
if os.name == "nt":
|
|
39
|
-
self._ipc = rf"\\.\pipe\aurclips-mpv-{os.getpid()}"
|
|
40
|
-
else:
|
|
41
|
-
self._ipc = str(Path(tempfile.gettempdir()) / f"aurclips-mpv-{os.getpid()}.sock")
|
|
42
|
-
# stdin=DEVNULL: el terminal es de la sesión de marcado; sin esto mpv
|
|
43
|
-
# heredaría la consola y competiría con el hilo lector por las teclas
|
|
44
|
-
self._proc = subprocess.Popen(
|
|
45
|
-
[mpv_path, f"--input-ipc-server={self._ipc}", "--", str(video_path)],
|
|
46
|
-
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
47
|
-
stderr=subprocess.DEVNULL,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
def alive(self) -> bool:
|
|
51
|
-
return self._proc.poll() is None
|
|
52
|
-
|
|
53
|
-
def playback_time(self) -> float | None:
|
|
54
|
-
"""Posición de reproducción en segundos; None si no se pudo leer."""
|
|
55
|
-
request = json.dumps({"command": ["get_property", "playback-time"]}) + "\n"
|
|
56
|
-
try:
|
|
57
|
-
for line in self._roundtrip(request):
|
|
58
|
-
try:
|
|
59
|
-
payload = json.loads(line)
|
|
60
|
-
except json.JSONDecodeError:
|
|
61
|
-
continue
|
|
62
|
-
if "error" in payload: # respuesta a la petición (no un evento)
|
|
63
|
-
if payload["error"] == "success" and isinstance(
|
|
64
|
-
payload.get("data"), (int, float)):
|
|
65
|
-
return float(payload["data"])
|
|
66
|
-
return None
|
|
67
|
-
except OSError:
|
|
68
|
-
return None
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
def _roundtrip(self, request: str) -> list[str]:
|
|
72
|
-
"""Envía una petición y devuelve las líneas de respuesta."""
|
|
73
|
-
if os.name == "nt":
|
|
74
|
-
# el pipe lo crea mpv; abrir/escribir/leer por petición es lo robusto
|
|
75
|
-
with open(self._ipc, "r+b", buffering=0) as pipe:
|
|
76
|
-
pipe.write(request.encode("utf-8"))
|
|
77
|
-
return pipe.read(4096).decode("utf-8", "replace").splitlines()
|
|
78
|
-
import socket
|
|
79
|
-
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
80
|
-
sock.settimeout(1.0)
|
|
81
|
-
sock.connect(self._ipc)
|
|
82
|
-
sock.sendall(request.encode("utf-8"))
|
|
83
|
-
chunks = b""
|
|
84
|
-
while b"\n" not in chunks:
|
|
85
|
-
data = sock.recv(4096)
|
|
86
|
-
if not data:
|
|
87
|
-
break
|
|
88
|
-
chunks += data
|
|
89
|
-
return chunks.decode("utf-8", "replace").splitlines()
|
|
90
|
-
|
|
91
|
-
def close(self) -> None:
|
|
92
|
-
if self.alive():
|
|
93
|
-
self._proc.terminate()
|
|
94
|
-
try:
|
|
95
|
-
self._proc.wait(timeout=3)
|
|
96
|
-
except subprocess.TimeoutExpired:
|
|
97
|
-
self._proc.kill()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aurclips-0.2.0 → aurclips-0.2.1}/docs/adr/0002-recortador-en-la-puerta-publicador-dentro.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|