kryten-webqueue 0.8.0__tar.gz → 0.8.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.
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/CHANGELOG.md +6 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/PKG-INFO +2 -1
- kryten_webqueue-0.8.1/kryten_webqueue/playlists/scheduler.py +131 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/pyproject.toml +2 -1
- kryten_webqueue-0.8.0/kryten_webqueue/playlists/scheduler.py +0 -72
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/.gitignore +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/README.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/config.example.json +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/ws/manager.py +0 -0
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
## [0.8.1] - 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **True RRULE-based recurring schedules.** Recurring schedules now auto-re-arm: after an automatic timed fire, the scheduler computes the next occurrence from the schedule's `rrule` (anchored on its fire time), advances `fire_at`, clears `fired_at`, and registers the next job — no manual re-arming needed. On startup, recurring schedules whose fire time elapsed while the service was down are advanced to their next future occurrence. Manual "Fire Now" intentionally does **not** advance the recurrence; the originally scheduled occurrence stays armed. Unparseable or exhausted rules are logged and left inert. Adds an explicit `python-dateutil` dependency for RRULE parsing.
|
|
12
|
+
|
|
7
13
|
## [0.8.0] - 2026-06-08
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kryten-webqueue
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube
|
|
5
5
|
Author: grobertson
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Requires-Dist: jinja2>=3.1
|
|
|
13
13
|
Requires-Dist: pillow>=10.0
|
|
14
14
|
Requires-Dist: pydantic>=2.0
|
|
15
15
|
Requires-Dist: pyjwt>=2.8
|
|
16
|
+
Requires-Dist: python-dateutil>=2.8
|
|
16
17
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
17
18
|
Requires-Dist: websockets>=12.0
|
|
18
19
|
Provides-Extra: dev
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
3
|
+
from apscheduler.triggers.date import DateTrigger
|
|
4
|
+
from datetime import datetime, UTC
|
|
5
|
+
|
|
6
|
+
from dateutil.rrule import rrulestr
|
|
7
|
+
|
|
8
|
+
from .fire import fire_schedule
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _next_occurrence(rrule_str: str, dtstart: datetime, after: datetime) -> datetime | None:
|
|
14
|
+
"""Return the next RRULE occurrence strictly after ``after``.
|
|
15
|
+
|
|
16
|
+
``dtstart`` anchors the recurrence pattern (typically the schedule's current
|
|
17
|
+
fire time). Returns None when the rule is exhausted or unparseable.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
rule = rrulestr(rrule_str, dtstart=dtstart)
|
|
21
|
+
return rule.after(after, inc=False)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
logger.warning(f"Could not compute next occurrence for rrule {rrule_str!r}: {e}")
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PlaylistScheduler:
|
|
28
|
+
"""APScheduler-based scheduler for playlist fire events."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, *, db, api_gate, shadow, ws_manager):
|
|
31
|
+
self._db = db
|
|
32
|
+
self._api_gate = api_gate
|
|
33
|
+
self._shadow = shadow
|
|
34
|
+
self._ws_manager = ws_manager
|
|
35
|
+
self._scheduler = AsyncIOScheduler()
|
|
36
|
+
|
|
37
|
+
async def start(self):
|
|
38
|
+
"""Start scheduler and load all pending schedules."""
|
|
39
|
+
self._scheduler.start()
|
|
40
|
+
await self._load_schedules()
|
|
41
|
+
logger.info("PlaylistScheduler started")
|
|
42
|
+
|
|
43
|
+
async def stop(self):
|
|
44
|
+
self._scheduler.shutdown(wait=False)
|
|
45
|
+
logger.info("PlaylistScheduler stopped")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _parse_fire_at(value: str) -> datetime:
|
|
49
|
+
"""Parse a stored fire_at ISO string into a UTC-aware datetime."""
|
|
50
|
+
dt = datetime.fromisoformat(value)
|
|
51
|
+
if dt.tzinfo is None:
|
|
52
|
+
dt = dt.replace(tzinfo=UTC)
|
|
53
|
+
return dt
|
|
54
|
+
|
|
55
|
+
async def _load_schedules(self):
|
|
56
|
+
"""Load active schedules from DB and register jobs.
|
|
57
|
+
|
|
58
|
+
Recurring schedules whose fire time has already passed (e.g. while the
|
|
59
|
+
service was down) are advanced to their next future occurrence.
|
|
60
|
+
"""
|
|
61
|
+
schedules = await self._db.get_schedules()
|
|
62
|
+
now = datetime.now(UTC)
|
|
63
|
+
for sched in schedules:
|
|
64
|
+
if not sched.get("is_active"):
|
|
65
|
+
continue
|
|
66
|
+
fire_at = self._parse_fire_at(sched["fire_at"])
|
|
67
|
+
if fire_at > now:
|
|
68
|
+
self._add_job(sched["id"], fire_at)
|
|
69
|
+
elif sched.get("is_recurring") and sched.get("rrule"):
|
|
70
|
+
nxt = _next_occurrence(sched["rrule"], fire_at, now)
|
|
71
|
+
if nxt:
|
|
72
|
+
nxt_utc = nxt.astimezone(UTC)
|
|
73
|
+
await self._db.update_schedule(
|
|
74
|
+
sched["id"], fire_at=nxt_utc.isoformat(), fired_at=None
|
|
75
|
+
)
|
|
76
|
+
self._add_job(sched["id"], nxt_utc)
|
|
77
|
+
logger.info(
|
|
78
|
+
f"Advanced missed recurring schedule {sched['id']} to {nxt_utc}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _add_job(self, schedule_id: int, fire_at: datetime):
|
|
82
|
+
job_id = f"schedule_{schedule_id}"
|
|
83
|
+
self._scheduler.add_job(
|
|
84
|
+
self._fire,
|
|
85
|
+
trigger=DateTrigger(run_date=fire_at),
|
|
86
|
+
id=job_id,
|
|
87
|
+
replace_existing=True,
|
|
88
|
+
kwargs={"schedule_id": schedule_id},
|
|
89
|
+
)
|
|
90
|
+
logger.info(f"Scheduled job {job_id} for {fire_at}")
|
|
91
|
+
|
|
92
|
+
async def _fire(self, schedule_id: int):
|
|
93
|
+
await fire_schedule(
|
|
94
|
+
schedule_id=schedule_id,
|
|
95
|
+
api_gate=self._api_gate,
|
|
96
|
+
db=self._db,
|
|
97
|
+
shadow=self._shadow,
|
|
98
|
+
ws_manager=self._ws_manager,
|
|
99
|
+
)
|
|
100
|
+
# After an automatic timed fire, advance recurring schedules to their
|
|
101
|
+
# next occurrence and re-arm. (Manual "Fire Now" does NOT advance the
|
|
102
|
+
# recurrence — the originally scheduled occurrence stays armed.)
|
|
103
|
+
await self._reschedule_if_recurring(schedule_id)
|
|
104
|
+
|
|
105
|
+
async def _reschedule_if_recurring(self, schedule_id: int):
|
|
106
|
+
sched = await self._db.get_schedule(schedule_id)
|
|
107
|
+
if not sched or not sched.get("is_active"):
|
|
108
|
+
return
|
|
109
|
+
if not sched.get("is_recurring") or not sched.get("rrule"):
|
|
110
|
+
return
|
|
111
|
+
fired_from = self._parse_fire_at(sched["fire_at"])
|
|
112
|
+
nxt = _next_occurrence(sched["rrule"], fired_from, fired_from)
|
|
113
|
+
if not nxt:
|
|
114
|
+
logger.info(f"Recurring schedule {schedule_id} has no further occurrences")
|
|
115
|
+
return
|
|
116
|
+
nxt_utc = nxt.astimezone(UTC)
|
|
117
|
+
await self._db.update_schedule(
|
|
118
|
+
schedule_id, fire_at=nxt_utc.isoformat(), fired_at=None
|
|
119
|
+
)
|
|
120
|
+
self._add_job(schedule_id, nxt_utc)
|
|
121
|
+
logger.info(f"Recurring schedule {schedule_id} re-armed for {nxt_utc}")
|
|
122
|
+
|
|
123
|
+
async def add_schedule(self, schedule_id: int, fire_at: datetime):
|
|
124
|
+
self._add_job(schedule_id, fire_at)
|
|
125
|
+
|
|
126
|
+
async def remove_schedule(self, schedule_id: int):
|
|
127
|
+
job_id = f"schedule_{schedule_id}"
|
|
128
|
+
try:
|
|
129
|
+
self._scheduler.remove_job(job_id)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kryten-webqueue"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.1"
|
|
4
4
|
description = "Netflix/Tubi-style catalog browser and pay-to-play queue management for CyTube"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -19,6 +19,7 @@ dependencies = [
|
|
|
19
19
|
"jinja2>=3.1",
|
|
20
20
|
"websockets>=12.0",
|
|
21
21
|
"pydantic>=2.0",
|
|
22
|
+
"python-dateutil>=2.8",
|
|
22
23
|
]
|
|
23
24
|
|
|
24
25
|
[project.optional-dependencies]
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
3
|
-
from apscheduler.triggers.date import DateTrigger
|
|
4
|
-
from datetime import datetime, UTC
|
|
5
|
-
|
|
6
|
-
from .fire import fire_schedule
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class PlaylistScheduler:
|
|
12
|
-
"""APScheduler-based scheduler for playlist fire events."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, *, db, api_gate, shadow, ws_manager):
|
|
15
|
-
self._db = db
|
|
16
|
-
self._api_gate = api_gate
|
|
17
|
-
self._shadow = shadow
|
|
18
|
-
self._ws_manager = ws_manager
|
|
19
|
-
self._scheduler = AsyncIOScheduler()
|
|
20
|
-
|
|
21
|
-
async def start(self):
|
|
22
|
-
"""Start scheduler and load all pending schedules."""
|
|
23
|
-
self._scheduler.start()
|
|
24
|
-
await self._load_schedules()
|
|
25
|
-
logger.info("PlaylistScheduler started")
|
|
26
|
-
|
|
27
|
-
async def stop(self):
|
|
28
|
-
self._scheduler.shutdown(wait=False)
|
|
29
|
-
logger.info("PlaylistScheduler stopped")
|
|
30
|
-
|
|
31
|
-
async def _load_schedules(self):
|
|
32
|
-
"""Load active schedules from DB and register jobs."""
|
|
33
|
-
schedules = await self._db.get_schedules()
|
|
34
|
-
now = datetime.now(UTC)
|
|
35
|
-
for sched in schedules:
|
|
36
|
-
if not sched.get("is_active"):
|
|
37
|
-
continue
|
|
38
|
-
fire_at_str = sched["fire_at"]
|
|
39
|
-
fire_at = datetime.fromisoformat(fire_at_str)
|
|
40
|
-
if fire_at <= now:
|
|
41
|
-
continue
|
|
42
|
-
self._add_job(sched["id"], fire_at)
|
|
43
|
-
|
|
44
|
-
def _add_job(self, schedule_id: int, fire_at: datetime):
|
|
45
|
-
job_id = f"schedule_{schedule_id}"
|
|
46
|
-
self._scheduler.add_job(
|
|
47
|
-
self._fire,
|
|
48
|
-
trigger=DateTrigger(run_date=fire_at),
|
|
49
|
-
id=job_id,
|
|
50
|
-
replace_existing=True,
|
|
51
|
-
kwargs={"schedule_id": schedule_id},
|
|
52
|
-
)
|
|
53
|
-
logger.info(f"Scheduled job {job_id} for {fire_at}")
|
|
54
|
-
|
|
55
|
-
async def _fire(self, schedule_id: int):
|
|
56
|
-
await fire_schedule(
|
|
57
|
-
schedule_id=schedule_id,
|
|
58
|
-
api_gate=self._api_gate,
|
|
59
|
-
db=self._db,
|
|
60
|
-
shadow=self._shadow,
|
|
61
|
-
ws_manager=self._ws_manager,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
async def add_schedule(self, schedule_id: int, fire_at: datetime):
|
|
65
|
-
self._add_job(schedule_id, fire_at)
|
|
66
|
-
|
|
67
|
-
async def remove_schedule(self, schedule_id: int):
|
|
68
|
-
job_id = f"schedule_{schedule_id}"
|
|
69
|
-
try:
|
|
70
|
-
self._scheduler.remove_job(job_id)
|
|
71
|
-
except Exception:
|
|
72
|
-
pass
|
|
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
|
|
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
|
{kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.8.0 → kryten_webqueue-0.8.1}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|