Skip to content

Commit

Permalink
wip split daemon and polling cli flags
Browse files Browse the repository at this point in the history
  • Loading branch information
purarue committed Aug 26, 2023
1 parent 9f2973b commit 46abe12
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 26 deletions.
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ mpv_history_daemon_restart /your/data/dir
```
Usage: mpv-history-daemon daemon [OPTIONS] SOCKET_DIR DATA_DIR
Socket dir is the directory with mpv sockets (/tmp/mpvsockets, probably)
Data dir is the directory to store the history JSON files
Socket dir is the directory with mpv sockets (/tmp/mpvsockets, probably) Data dir is the
directory to store the history JSON files
Options:
--log-file PATH location of logfile
--write-period INTEGER How often to write to files while mpv is open
--help Show this message and exit.
--log-file PATH location of logfile
--write-period INTEGER How often to write JSON data files while mpv is open
-s, --scan-time INTEGER How often to scan for new mpv sockets files in the SOCKET_DIR -
this is a manual scan of the directory every <n> seconds [env
var: MPV_HISTORY_DAEMON_SCAN_TIME; default: 10]
--socket-class-qualname TEXT Fully qualified name of the class to use for socket data, e.g.,
'mpv_history_daemon.daemon.SocketData'. This imports the class and
uses it for socket data.
--help Show this message and exit.
```

Some logs, to get an idea of what this captures:
Expand All @@ -57,7 +63,7 @@ Connected refused for socket at /tmp/mpvsockets/1598956534118491075, removing de

More events would keep getting logged, as I pause/play, or the file ends and a new file starts. The key for each JSON value is the epoch time, so everything is timestamped.

By default, this scans the socket directory every 10 seconds -- to increase that you can set the `MPV_HISTORY_DAEMON_SCAN_TIME` environment variable, e.g. `MPV_HISTORY_DAEMON_SCAN_TIME=5`
By default, this scans the socket directory every 10 seconds. If you'd instead like to use a filewatcher so this picks up files immediately, you can install `python3 -m pip install watchfiles` and use `--watch`

#### custom SocketData class

Expand All @@ -70,15 +76,13 @@ You can pass a custom socket data class with to `daemon` with `--socket-class-qu
The daemon saves the raw event data above in JSON files, which can then be parsed into individual instances of media:

```
$ mpv-history-daemon parse --help
Usage: mpv-history-daemon parse [OPTIONS] DATA_DIR
Usage: mpv-history-daemon parse [OPTIONS] DATA_FILES...
Takes the data directory and parses events into Media
Options:
--all-events return all events, even ones which by context you probably
didn't listen to
--debug Increase log verbosity/print warnings while parsing JSON files
--help Show this message and exit.
```
Expand Down
35 changes: 32 additions & 3 deletions mpv_history_daemon/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
import importlib
from pathlib import Path
from typing import Any, Optional, Sequence, Iterator, Optional
from typing import Any, Optional, Sequence, Iterator, Optional, Union, Literal
from tempfile import gettempdir

import click
Expand All @@ -18,14 +18,30 @@
from . import events as events_module


@click.group()
@click.group(context_settings={"max_content_width": 100})
def cli():
"""
Connects to mpv socket files and saves a history of events
"""
pass


def _parse_polling(
ctx: click.Context,
param: click.Parameter,
value: Union[str, int],
) -> Union[Literal["disabled"], int]:
if value == "disabled":
return "disabled"
if isinstance(value, int):
return value
if isinstance(value, str) and value.strip().isdigit():
return int(value)
raise click.BadParameter(
f"must be an integer or 'disabled', got {value!r}", ctx=ctx, param=param
)


@cli.command()
@click.argument("SOCKET_DIR")
@click.argument("DATA_DIR")
Expand All @@ -39,7 +55,17 @@ def cli():
"--write-period",
type=int,
default=None,
help="How often to write to files while mpv is open",
help="How often to write JSON data files while mpv is open",
)
@click.option(
"-s",
"--scan-time",
envvar="MPV_HISTORY_DAEMON_SCAN_TIME",
show_envvar=True,
default=10,
type=click.UNPROCESSED,
callback=_parse_polling,
help="How often to scan for new mpv sockets files in the SOCKET_DIR - this is a manual scan of the directory every <n> seconds (default: 10). Set to 'disabled' to disable polling (if you're using --watch).",
)
@click.option(
"--socket-class-qualname",
Expand All @@ -51,6 +77,7 @@ def daemon(
socket_dir: str,
data_dir: str,
log_file: str,
scan_time: Union[Literal["disabled"], int],
write_period: Optional[int],
socket_class_qualname: Optional[str],
) -> None:
Expand All @@ -64,12 +91,14 @@ def daemon(
module = importlib.import_module(module_name)
socketclass = getattr(module, class_name)
assert issubclass(socketclass, SocketData)
poll_time = scan_time if isinstance(scan_time, int) else None
run(
socket_dir=socket_dir,
data_dir=data_dir,
log_file=log_file,
write_period=write_period,
socket_data_cls=socketclass,
poll_time=poll_time,
)


Expand Down
19 changes: 12 additions & 7 deletions mpv_history_daemon/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def __init__(
*,
autostart: bool = True,
write_period: Optional[int],
poll_time: Optional[int] = 10,
socket_data_cls: Type[SocketData] = SocketData,
):
self.data_dir: str = data_dir
Expand All @@ -283,6 +284,7 @@ def __init__(
self._socket_dir_path: Path = Path(socket_dir).expanduser().absolute()
self.sockets: Dict[str, MPV] = {}
self.socket_data_cls = socket_data_cls
self.poll_time = poll_time
self.socket_data: Dict[str, SocketData] = {}
if autostart:
self.run_loop()
Expand Down Expand Up @@ -442,13 +444,14 @@ def write_data(self, force: bool = False) -> None:
socket_data.write()

def run_loop(self) -> None:
logger.debug("Starting mpv-history-daemon loop...")
logger.debug(f"Using socket class {self.socket_data_cls}")
while True:
self.scan_sockets()
self.periodic_write()
self.write_data()
sleep(SCAN_TIME)
if self.poll_time:
logger.debug("Starting mpv-history-daemon loop...")
logger.debug(f"Using socket class {self.socket_data_cls}")
while True:
self.scan_sockets()
self.periodic_write()
self.write_data()
sleep(self.poll_time)


def run(
Expand All @@ -457,6 +460,7 @@ def run(
log_file: str,
write_period: Optional[int],
socket_data_cls: Type[SocketData],
poll_time: Optional[int],
) -> None:
# if the daemon launched before any mpv instances
if not os.path.exists(socket_dir):
Expand All @@ -471,6 +475,7 @@ def run(
autostart=False,
write_period=write_period,
socket_data_cls=socket_data_cls,
poll_time=poll_time,
)
# incase user keyboardinterrupt's or this crashes completely
# for some reason, write data out to files in-case it hasn't
Expand Down
20 changes: 14 additions & 6 deletions mpv_history_daemon/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

from logzero import setup_logger # type: ignore[import]

from .daemon import SCAN_TIME
from .serialize import parse_json_file

# TODO: better logger setup?
Expand All @@ -46,7 +45,8 @@ class Action(NamedTuple):
# this event happened this many seconds after this media was started
since_started: float
action: str
percentage: Optional[float] # this can be None if its a livestream, those dont have a percent-pos
# this can be None if its a livestream, those dont have a percent-pos
percentage: Optional[float]


class Media(NamedTuple):
Expand Down Expand Up @@ -342,7 +342,9 @@ def _reconstruct_event_stream(
# the user hasnt pasued/played since actions is 0, so this must be the resumed event that
# happens when a socket first connects. And since it is not paused and we've seen a pause event,
# we are sure that the media is already playing
logger.debug("We've seen an is-paused event and the file is already playing, ignoring resume event")
logger.debug(
"We've seen an is-paused event and the file is already playing, ignoring resume event"
)
else:
actions[dt_float] = (event_name, event_data["percent-pos"])

Expand All @@ -354,22 +356,28 @@ def _reconstruct_event_stream(
# the last data I have that actually uses this code is from 2021-03-18 16:52:45.565000
# https://github.com/seanbreckenridge/mpv-history-daemon/commit/451afb4d841262cfe0aa1a6f81fd44ef110407f6


# this is an old file, so we have to guess if the resume was correct by checking if it was within
# the first 20 seconds (would be 10, but lets give double that for the scan time/possibly rebooting daemon)
# of the file (the default for older versions of the daemon)
#
# if it was, then this is the 'resumed' event that happens when a socket first connects
# if it wasnt, then this is a resume event that happened after the file was paused
# so we should add it to the actions
if start_time is not None and dt_float - start_time <= 20 and is_playing and len(actions) == 0:
if (
start_time is not None
and dt_float - start_time <= 20
and is_playing
and len(actions) == 0
):
# this was in the first 10 seconds of the file, and its already playing, so
# lets assume this is the 'resumed' event that happens when a socket first connects
# and ignore it
#
# this should be fine anyways, as its just the action we're ignoring here, the file
# is already playing and we received a resume event, so we are not changing the state
logger.debug("Ignoring resume event in the first 20 seconds of the file while we are already playing, we cant know if this is a real resume event or not")
logger.debug(
"Ignoring resume event in the first 20 seconds of the file while we are already playing, we cant know if this is a real resume event or not"
)
else:
# this might have also been a case in which mpv was already playing and you started the daemon afterwards
# if playlist position is higher than 0, then this was probably already paused mpv connected (but this is an old file)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
long_description_content_type="text/markdown",
install_requires=requirements,
python_requires=">=3.8",
extras_require={"optional": ["orjson"]},
extras_require={"optional": ["orjson", "watchfiles"]},
name=pkg,
packages=find_packages(include=[pkg]),
package_data={pkg: ["py.typed"]},
Expand Down

0 comments on commit 46abe12

Please sign in to comment.