aboutsummaryrefslogtreecommitdiff
path: root/jellyfin_apiclient_python/timesync_manager.py
blob: 5c4e98e029b8c705fdca19dff33693dcc3f68bfd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# This is based on https://github.com/jellyfin/jellyfin-web/blob/master/src/components/syncPlay/timeSyncManager.js
import threading
import logging
import datetime

LOG = logging.getLogger('Jellyfin.' + __name__)

number_of_tracked_measurements = 8
polling_interval_greedy = 1
polling_interval_low_profile = 60
greedy_ping_count = 3


class Measurement:
    def __init__(self, request_sent, request_received, response_sent, response_received):
        self.request_sent = request_sent
        self.request_received = request_received
        self.response_sent = response_sent
        self.response_received = response_received

    def get_offset(self):
        """Time offset from server."""
        return ((self.request_received - self.request_sent) + (self.response_sent - self.response_received)) / 2.0

    def get_delay(self):
        """Get round-trip delay."""
        return (self.response_received - self.request_sent) - (self.response_sent - self.request_received)

    def get_ping(self):
        """Get ping time."""
        return self.get_delay() / 2.0


class _TimeSyncThread(threading.Thread):
    def __init__(self, manager):
        self.manager = manager
        self.halt = threading.Event()
        threading.Thread.__init__(self)

    def run(self):
        while not self.halt.wait(self.manager.polling_interval):
            try:
                measurement = self.manager.client.jellyfin.utc_time()
                measurement = Measurement(measurement["request_sent"], measurement["request_received"],
                                          measurement["response_sent"], measurement["response_received"])

                self.manager.update_time_offset(measurement)

                if self.manager.pings > greedy_ping_count:
                    self.manager.polling_interval = polling_interval_low_profile
                else:
                    self.manager.pings += 1

                self.manager._notify_subscribers()
            except Exception:
                LOG.error("Timesync call failed.", exc_info=True)

    def stop(self):
        self.halt.set()
        self.join()


class TimeSyncManager:
    def __init__(self, client):
        self.ping_stop = True
        self.polling_interval = polling_interval_greedy
        self.poller = None
        self.pings = 0  # number of pings
        self.measurement = None  # current time sync
        self.measurements = []
        self.client = client
        self.timesync_thread = None
        self.subscribers = set()

    def is_ready(self):
        """Gets status of time sync."""
        return self.measurement is not None

    def get_time_offset(self):
        """Gets time offset with server."""
        return self.measurement.get_offset() if self.measurement is not None else datetime.timedelta(0)

    def get_ping(self):
        """Gets ping time to server."""
        return self.measurement.get_ping() if self.measurement is not None else datetime.timedelta(0)

    def update_time_offset(self, measurement):
        """Updates time offset between server and client."""
        self.measurements.append(measurement)
        if len(self.measurements) > number_of_tracked_measurements:
            self.measurements.pop(0)

        self.measurement = min(self.measurements, key=lambda x: x.get_delay())

    def reset_measurements(self):
        """Drops accumulated measurements."""
        self.measurement = None
        self.measurements = []

    def start_ping(self):
        """Starts the time poller."""
        if not self.timesync_thread:
            self.timesync_thread = _TimeSyncThread(self)
            self.timesync_thread.start()

    def stop_ping(self):
        """Stops the time poller."""
        if self.timesync_thread:
            self.timesync_thread.stop()
            self.timesync_thread = None

    def force_update(self):
        """Resets poller into greedy mode."""
        self.stop_ping()
        self.polling_interval = polling_interval_greedy
        self.pings = 0
        self.start_ping()

    def server_date_to_local(self, server):
        """Converts server time to local time."""
        return server - self.get_time_offset()

    def local_date_to_server(self, local):
        """Converts local time to server time."""
        return local + self.get_time_offset()

    def subscribe_time_offset(self, subscriber_callable):
        """Pass a callback function to get notified about time offset changes."""
        self.subscribers.add(subscriber_callable)

    def remove_subscriber(self, subscriber_callable):
        """Remove a callback function from notifications."""
        self.subscribers.remove(subscriber_callable)

    def _notify_subscribers(self):
        for subscriber in self.subscribers:
            try:
                subscriber(self.get_time_offset(), self.get_ping())
            except Exception:
                LOG.error("Exception in subscriber callback.")