forked from cedric-marcoux/dispatcharr_timeshift
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin.py
More file actions
410 lines (371 loc) · 21 KB
/
Copy pathplugin.py
File metadata and controls
410 lines (371 loc) · 21 KB
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
"""
Dispatcharr Timeshift Plugin
Adds timeshift/catch-up TV support for Xtream Codes providers,
allowing users to watch past TV programs (typically up to 7 days).
GitHub: https://github.com/cedric-marcoux/dispatcharr_timeshift
AUTO-INSTALL ON STARTUP:
This module auto-installs hooks when loaded if the plugin is enabled.
Dispatcharr's PluginManager imports this module on startup, triggering
the auto-install code at the bottom of this file.
IMPORTANT - uWSGI Multi-Worker Architecture:
Dispatcharr runs with multiple uWSGI workers (separate processes).
Each worker has its own memory space, so hooks must be installed
in EACH worker independently.
"""
import json
import logging
import os
logger = logging.getLogger("plugins.dispatcharr_timeshift")
# Track if hooks are installed in THIS worker (each uWSGI worker is separate)
_hooks_installed = False
def _auto_install_hooks():
"""
Install hooks automatically on Django startup.
Hooks are ALWAYS installed, but they check _is_plugin_enabled() at runtime.
This allows enabling/disabling the plugin without restart.
"""
global _hooks_installed
if _hooks_installed:
return
try:
from .hooks import install_hooks
if install_hooks():
_hooks_installed = True
logger.info("[Timeshift] Hooks installed (will check enabled state at runtime)")
except Exception as e:
logger.error(f"[Timeshift] Auto-install error: {e}")
def _read_plugin_version():
"""
Read version from plugin.json (single source of truth).
WHY plugin.json?
plugin.py self.version was often forgotten during releases,
causing mismatches. plugin.json is always updated by the
release process, so it's the authoritative version source.
"""
try:
manifest_path = os.path.join(os.path.dirname(__file__), "plugin.json")
with open(manifest_path, "r") as f:
return json.load(f).get("version", "0.0.0")
except Exception:
return "0.0.0"
class Plugin:
"""
Main plugin class for Dispatcharr Timeshift.
Dispatcharr's PluginManager calls run() with action="enable" or "disable"
when the plugin is toggled in the UI.
"""
GITHUB_REPO = "cedric-marcoux/dispatcharr_timeshift"
def __init__(self):
self.name = "Dispatcharr Timeshift"
self.version = _read_plugin_version()
self.description = "Timeshift/catch-up TV support for Xtream Codes providers"
self.url = "https://github.com/cedric-marcoux/dispatcharr_timeshift"
self.author = "Cedric Marcoux"
self.author_url = "https://github.com/cedric-marcoux"
# Settings fields (version info is prepended dynamically via @property)
self._settings_fields = [
{
"id": "timezone",
"type": "select",
"label": "Provider Timezone",
"default": "Europe/Brussels",
"options": [
# UTC
{"value": "UTC", "label": "UTC"},
# Europe
{"value": "Europe/Amsterdam", "label": "Europe/Amsterdam (CET)"},
{"value": "Europe/Andorra", "label": "Europe/Andorra (CET)"},
{"value": "Europe/Athens", "label": "Europe/Athens (EET)"},
{"value": "Europe/Belgrade", "label": "Europe/Belgrade (CET)"},
{"value": "Europe/Berlin", "label": "Europe/Berlin (CET)"},
{"value": "Europe/Bratislava", "label": "Europe/Bratislava (CET)"},
{"value": "Europe/Brussels", "label": "Europe/Brussels (CET)"},
{"value": "Europe/Bucharest", "label": "Europe/Bucharest (EET)"},
{"value": "Europe/Budapest", "label": "Europe/Budapest (CET)"},
{"value": "Europe/Chisinau", "label": "Europe/Chisinau (EET)"},
{"value": "Europe/Copenhagen", "label": "Europe/Copenhagen (CET)"},
{"value": "Europe/Dublin", "label": "Europe/Dublin (GMT/IST)"},
{"value": "Europe/Gibraltar", "label": "Europe/Gibraltar (CET)"},
{"value": "Europe/Helsinki", "label": "Europe/Helsinki (EET)"},
{"value": "Europe/Istanbul", "label": "Europe/Istanbul (TRT)"},
{"value": "Europe/Kaliningrad", "label": "Europe/Kaliningrad (EET)"},
{"value": "Europe/Kiev", "label": "Europe/Kiev (EET)"},
{"value": "Europe/Lisbon", "label": "Europe/Lisbon (WET)"},
{"value": "Europe/Ljubljana", "label": "Europe/Ljubljana (CET)"},
{"value": "Europe/London", "label": "Europe/London (GMT/BST)"},
{"value": "Europe/Luxembourg", "label": "Europe/Luxembourg (CET)"},
{"value": "Europe/Madrid", "label": "Europe/Madrid (CET)"},
{"value": "Europe/Malta", "label": "Europe/Malta (CET)"},
{"value": "Europe/Minsk", "label": "Europe/Minsk (MSK)"},
{"value": "Europe/Monaco", "label": "Europe/Monaco (CET)"},
{"value": "Europe/Moscow", "label": "Europe/Moscow (MSK)"},
{"value": "Europe/Oslo", "label": "Europe/Oslo (CET)"},
{"value": "Europe/Paris", "label": "Europe/Paris (CET)"},
{"value": "Europe/Podgorica", "label": "Europe/Podgorica (CET)"},
{"value": "Europe/Prague", "label": "Europe/Prague (CET)"},
{"value": "Europe/Riga", "label": "Europe/Riga (EET)"},
{"value": "Europe/Rome", "label": "Europe/Rome (CET)"},
{"value": "Europe/Samara", "label": "Europe/Samara (SAMT)"},
{"value": "Europe/San_Marino", "label": "Europe/San_Marino (CET)"},
{"value": "Europe/Sarajevo", "label": "Europe/Sarajevo (CET)"},
{"value": "Europe/Simferopol", "label": "Europe/Simferopol (MSK)"},
{"value": "Europe/Skopje", "label": "Europe/Skopje (CET)"},
{"value": "Europe/Sofia", "label": "Europe/Sofia (EET)"},
{"value": "Europe/Stockholm", "label": "Europe/Stockholm (CET)"},
{"value": "Europe/Tallinn", "label": "Europe/Tallinn (EET)"},
{"value": "Europe/Tirane", "label": "Europe/Tirane (CET)"},
{"value": "Europe/Vaduz", "label": "Europe/Vaduz (CET)"},
{"value": "Europe/Vatican", "label": "Europe/Vatican (CET)"},
{"value": "Europe/Vienna", "label": "Europe/Vienna (CET)"},
{"value": "Europe/Vilnius", "label": "Europe/Vilnius (EET)"},
{"value": "Europe/Volgograd", "label": "Europe/Volgograd (MSK)"},
{"value": "Europe/Warsaw", "label": "Europe/Warsaw (CET)"},
{"value": "Europe/Zagreb", "label": "Europe/Zagreb (CET)"},
{"value": "Europe/Zurich", "label": "Europe/Zurich (CET)"},
# America
{"value": "America/Anchorage", "label": "America/Anchorage (AKST)"},
{"value": "America/Argentina/Buenos_Aires", "label": "America/Buenos_Aires (ART)"},
{"value": "America/Bogota", "label": "America/Bogota (COT)"},
{"value": "America/Caracas", "label": "America/Caracas (VET)"},
{"value": "America/Chicago", "label": "America/Chicago (CST)"},
{"value": "America/Denver", "label": "America/Denver (MST)"},
{"value": "America/Halifax", "label": "America/Halifax (AST)"},
{"value": "America/Havana", "label": "America/Havana (CST)"},
{"value": "America/Lima", "label": "America/Lima (PET)"},
{"value": "America/Los_Angeles", "label": "America/Los_Angeles (PST)"},
{"value": "America/Mexico_City", "label": "America/Mexico_City (CST)"},
{"value": "America/Montreal", "label": "America/Montreal (EST)"},
{"value": "America/New_York", "label": "America/New_York (EST)"},
{"value": "America/Panama", "label": "America/Panama (EST)"},
{"value": "America/Phoenix", "label": "America/Phoenix (MST)"},
{"value": "America/Santiago", "label": "America/Santiago (CLT)"},
{"value": "America/Sao_Paulo", "label": "America/Sao_Paulo (BRT)"},
{"value": "America/St_Johns", "label": "America/St_Johns (NST)"},
{"value": "America/Toronto", "label": "America/Toronto (EST)"},
{"value": "America/Vancouver", "label": "America/Vancouver (PST)"},
# Asia
{"value": "Asia/Almaty", "label": "Asia/Almaty (ALMT)"},
{"value": "Asia/Amman", "label": "Asia/Amman (EET)"},
{"value": "Asia/Baghdad", "label": "Asia/Baghdad (AST)"},
{"value": "Asia/Baku", "label": "Asia/Baku (AZT)"},
{"value": "Asia/Bangkok", "label": "Asia/Bangkok (ICT)"},
{"value": "Asia/Beirut", "label": "Asia/Beirut (EET)"},
{"value": "Asia/Colombo", "label": "Asia/Colombo (IST)"},
{"value": "Asia/Damascus", "label": "Asia/Damascus (EET)"},
{"value": "Asia/Dhaka", "label": "Asia/Dhaka (BST)"},
{"value": "Asia/Dubai", "label": "Asia/Dubai (GST)"},
{"value": "Asia/Ho_Chi_Minh", "label": "Asia/Ho_Chi_Minh (ICT)"},
{"value": "Asia/Hong_Kong", "label": "Asia/Hong_Kong (HKT)"},
{"value": "Asia/Jakarta", "label": "Asia/Jakarta (WIB)"},
{"value": "Asia/Jerusalem", "label": "Asia/Jerusalem (IST)"},
{"value": "Asia/Kabul", "label": "Asia/Kabul (AFT)"},
{"value": "Asia/Karachi", "label": "Asia/Karachi (PKT)"},
{"value": "Asia/Kathmandu", "label": "Asia/Kathmandu (NPT)"},
{"value": "Asia/Kolkata", "label": "Asia/Kolkata (IST)"},
{"value": "Asia/Kuala_Lumpur", "label": "Asia/Kuala_Lumpur (MYT)"},
{"value": "Asia/Kuwait", "label": "Asia/Kuwait (AST)"},
{"value": "Asia/Manila", "label": "Asia/Manila (PHT)"},
{"value": "Asia/Muscat", "label": "Asia/Muscat (GST)"},
{"value": "Asia/Nicosia", "label": "Asia/Nicosia (EET)"},
{"value": "Asia/Qatar", "label": "Asia/Qatar (AST)"},
{"value": "Asia/Riyadh", "label": "Asia/Riyadh (AST)"},
{"value": "Asia/Seoul", "label": "Asia/Seoul (KST)"},
{"value": "Asia/Shanghai", "label": "Asia/Shanghai (CST)"},
{"value": "Asia/Singapore", "label": "Asia/Singapore (SGT)"},
{"value": "Asia/Taipei", "label": "Asia/Taipei (CST)"},
{"value": "Asia/Tashkent", "label": "Asia/Tashkent (UZT)"},
{"value": "Asia/Tehran", "label": "Asia/Tehran (IRST)"},
{"value": "Asia/Tokyo", "label": "Asia/Tokyo (JST)"},
{"value": "Asia/Yekaterinburg", "label": "Asia/Yekaterinburg (YEKT)"},
# Africa
{"value": "Africa/Algiers", "label": "Africa/Algiers (CET)"},
{"value": "Africa/Cairo", "label": "Africa/Cairo (EET)"},
{"value": "Africa/Casablanca", "label": "Africa/Casablanca (WET)"},
{"value": "Africa/Johannesburg", "label": "Africa/Johannesburg (SAST)"},
{"value": "Africa/Lagos", "label": "Africa/Lagos (WAT)"},
{"value": "Africa/Nairobi", "label": "Africa/Nairobi (EAT)"},
{"value": "Africa/Tunis", "label": "Africa/Tunis (CET)"},
# Australia & Pacific
{"value": "Australia/Adelaide", "label": "Australia/Adelaide (ACST)"},
{"value": "Australia/Brisbane", "label": "Australia/Brisbane (AEST)"},
{"value": "Australia/Darwin", "label": "Australia/Darwin (ACST)"},
{"value": "Australia/Hobart", "label": "Australia/Hobart (AEST)"},
{"value": "Australia/Melbourne", "label": "Australia/Melbourne (AEST)"},
{"value": "Australia/Perth", "label": "Australia/Perth (AWST)"},
{"value": "Australia/Sydney", "label": "Australia/Sydney (AEST)"},
{"value": "Pacific/Auckland", "label": "Pacific/Auckland (NZST)"},
{"value": "Pacific/Fiji", "label": "Pacific/Fiji (FJT)"},
{"value": "Pacific/Honolulu", "label": "Pacific/Honolulu (HST)"},
],
"help_text": "Timezone for timestamp conversion (must match your XC provider's timezone)"
},
{
"id": "language",
"type": "select",
"label": "EPG Language",
"default": "en",
"options": [
{"value": "bg", "label": "Български (Bulgarian)"},
{"value": "cs", "label": "Čeština (Czech)"},
{"value": "da", "label": "Dansk (Danish)"},
{"value": "de", "label": "Deutsch"},
{"value": "el", "label": "Ελληνικά (Greek)"},
{"value": "en", "label": "English"},
{"value": "es", "label": "Español"},
{"value": "et", "label": "Eesti (Estonian)"},
{"value": "fi", "label": "Suomi (Finnish)"},
{"value": "fr", "label": "Français"},
{"value": "hr", "label": "Hrvatski (Croatian)"},
{"value": "hu", "label": "Magyar (Hungarian)"},
{"value": "it", "label": "Italiano"},
{"value": "lt", "label": "Lietuvių (Lithuanian)"},
{"value": "lv", "label": "Latviešu (Latvian)"},
{"value": "nl", "label": "Nederlands"},
{"value": "no", "label": "Norsk (Norwegian)"},
{"value": "pl", "label": "Polski (Polish)"},
{"value": "pt", "label": "Português"},
{"value": "ro", "label": "Română (Romanian)"},
{"value": "ru", "label": "Русский (Russian)"},
{"value": "sk", "label": "Slovenčina (Slovak)"},
{"value": "sl", "label": "Slovenščina (Slovenian)"},
{"value": "sr", "label": "Српски (Serbian)"},
{"value": "sv", "label": "Svenska (Swedish)"},
{"value": "tr", "label": "Türkçe (Turkish)"},
{"value": "uk", "label": "Українська (Ukrainian)"},
],
"help_text": "Language code for EPG data (ISO 639-1)"
},
{
"id": "debug_mode",
"type": "boolean",
"label": "Debug Mode",
"default": False,
"help_text": "Enable ultra-verbose logging for troubleshooting (check Dispatcharr logs)"
},
{
"id": "url_format",
"type": "select",
"label": "Catchup URL Format",
"default": "auto",
"options": [
{"value": "auto", "label": "Auto-detect (A → B fallback)"},
{"value": "format_a", "label": "Format A (query string: timeshift.php?...)"},
{"value": "format_b", "label": "Format B (path: /timeshift/user/pass/...)"},
{"value": "custom", "label": "Custom template"}
],
"help_text": "URL format for timeshift requests. Auto-detect works for most providers."
},
{
"id": "custom_url_template",
"type": "string",
"label": "Custom URL Template",
"default": "",
"help_text": (
"Only used when 'Custom template' is selected. "
"Example: {server_url}/streaming/timeshift.php?username={username}&password={password}"
"&stream={stream_id}&start={timestamp}&duration={duration} — "
"Placeholders: {server_url} {username} {password} {stream_id} {timestamp} (local time YYYY-MM-DD:HH-MM) "
"{duration} (minutes from EPG) {start_unix} (Unix epoch) "
"{epg_channel_id} {channel_name} {channel_id} {tv_archive_duration} (days) {extension} (ts/m3u8)"
)
}
]
# No custom actions needed
self.actions = []
@property
def fields(self):
"""
Dynamic fields: version check info + settings fields.
WHY @property?
The version check is lazy: only triggered when the settings page
loads (PluginManager reads plugin.instance.fields). The result is
cached 24h in memory, so subsequent loads are instant.
Zero changes needed in Dispatcharr core.
"""
version_field = self._build_version_field()
return [version_field] + self._settings_fields
def _build_version_field(self):
"""Build the info field showing version status with GitHub link."""
try:
from .version_check import check_for_update
result = check_for_update(self.GITHUB_REPO, self.version)
except Exception:
result = {"error": "Version check module unavailable", "current": self.version}
if result.get("error"):
return {
"id": "version_info",
"type": "info",
"label": f"Version {result['current']}",
"help_text": f"Unable to check for updates: {result['error']}",
}
elif result.get("has_update"):
return {
"id": "version_info",
"type": "info",
"label": f"Update available: v{result['latest']}",
"help_text": (
f"You are running v{result['current']}. "
f"Version {result['latest']} is available. "
f"Download: {result['release_url']}"
),
}
else:
return {
"id": "version_info",
"type": "info",
"label": f"Version {result['current']} (latest)",
"help_text": f"You are running the latest version. Last checked: {result.get('checked_at', 'N/A')}",
}
def run(self, action=None, params=None, context=None):
"""
Execute plugin action.
Called by PluginManager when:
- action="enable": Plugin is being enabled
- action="disable": Plugin is being disabled
"""
global _hooks_installed
context = context or {}
if action == "enable":
logger.info("[Timeshift] Enabling plugin...")
from .hooks import install_hooks
if install_hooks():
_hooks_installed = True
return {"status": "ok", "message": "Timeshift plugin enabled"}
return {"status": "error", "message": "Failed to install hooks"}
elif action == "disable":
logger.info("[Timeshift] Disabling plugin...")
from .hooks import uninstall_hooks
uninstall_hooks()
_hooks_installed = False
return {"status": "ok", "message": "Timeshift plugin disabled"}
return {"status": "error", "message": f"Unknown action: {action}"}
def stop(self, context=None):
"""
Graceful shutdown - called by Dispatcharr v0.19+ on disable/reload/delete.
Args:
context (dict, optional): Contains 'settings', 'logger', 'reason', 'actions'
"""
global _hooks_installed
reason = context.get("reason", "unknown") if context else "unknown"
logger.info(f"[Timeshift] Stopping plugin (reason: {reason})...")
from .hooks import uninstall_hooks
if uninstall_hooks():
_hooks_installed = False
logger.info(f"[Timeshift] Plugin stopped successfully (reason: {reason})")
return {"status": "ok", "message": f"Timeshift stopped (reason: {reason})"}
return {"status": "error", "message": "Failed to uninstall hooks"}
# Auto-install hooks when this module is imported (on Django startup)
# This runs once per uWSGI worker when PluginManager discovers this plugin
try:
import django
if django.apps.apps.ready:
_auto_install_hooks()
else:
# Django not ready yet, use signal to install on first request
from django.core.signals import request_finished
def _on_first_request(sender, **kwargs):
_auto_install_hooks()
request_finished.disconnect(_on_first_request)
request_finished.connect(_on_first_request)
except Exception:
pass