kryten-webqueue 0.6.2__tar.gz → 0.6.3__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.6.2 → kryten_webqueue-0.6.3}/CHANGELOG.md +17 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/PKG-INFO +1 -1
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/queue/shadow.py +29 -8
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/static/css/main.css +40 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/auth/login.html +2 -1
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/queue/index.html +17 -2
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/pyproject.toml +1 -1
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/.gitignore +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/README.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/config.example.json +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/catalog/db.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/ws/manager.py +0 -0
|
@@ -4,7 +4,24 @@ 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.6.3] - 2026-06-08
|
|
7
8
|
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Queue items missing title/duration for externally-queued media** — CyTube playlist items nest their metadata under a `media` key (`{uid, temp, queueby, media: {id, title, seconds, type}}`), but the shadow reconciler read flat fields, so any item not added by the running webqueue instance showed "Unknown" with a `0:00` duration. The reconciler now reads the nested `media` object (with a flat-key fallback) and backfills title/duration/media-id onto known items. Externally-queued items now also show their CyTube `queueby` as the requester.
|
|
12
|
+
- **Now Playing runtime showing `NaN:NaN`** — The now-playing total used the preformatted `duration` string instead of the numeric `seconds`, producing `NaN:NaN` for the total and remaining time. It now uses `seconds` (with a numeric fallback).
|
|
13
|
+
- **Estimated start times** — Now derived from the numeric now-playing `seconds`/`currentTime`, so the queue ETAs are correct now that item durations are read properly.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Sticky, highlighted Now Playing card** — The Now Playing card is pinned below the navbar while scrolling a long queue, is highlighted with a subtle accent ring/glow, and the page auto-scrolls it into view on load.
|
|
18
|
+
- **Case-sensitivity notice on login** — The OTP login view now warns that the CyTube username is case-sensitive and must match exactly (e.g. `TacoBelmont` ≠ `tacobelmont`); the username field no longer auto-capitalizes or autocorrects.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Sticky navbar hardening** — Added `scroll-padding-top` (via a `--nav-height` variable) so scrolled-to content clears the sticky header.
|
|
23
|
+
|
|
24
|
+
[0.6.3]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.6.3
|
|
8
25
|
## [0.6.2] - 2026-06-07
|
|
9
26
|
|
|
10
27
|
### Fixed
|
|
@@ -63,20 +63,40 @@ class QueueShadow:
|
|
|
63
63
|
|
|
64
64
|
for pos, polled in enumerate(playlist_items):
|
|
65
65
|
uid = polled["uid"]
|
|
66
|
+
# CyTube playlist items nest the media metadata under a "media"
|
|
67
|
+
# key ({uid, temp, queueby, media: {id, title, seconds, type}}).
|
|
68
|
+
# Fall back to flat keys for forward/backward compatibility.
|
|
69
|
+
media = polled.get("media") if isinstance(polled.get("media"), dict) else polled
|
|
70
|
+
title = media.get("title") or polled.get("title") or ""
|
|
71
|
+
media_type = media.get("type") or polled.get("type") or "unknown"
|
|
72
|
+
media_id = media.get("id") or polled.get("id") or ""
|
|
73
|
+
duration_sec = _to_seconds(media.get("seconds", media.get("duration")))
|
|
74
|
+
queueby = polled.get("queueby") or None
|
|
75
|
+
|
|
66
76
|
if uid in local_map:
|
|
67
|
-
# Preserve local metadata, update position
|
|
68
|
-
|
|
77
|
+
# Preserve local metadata, update position; backfill any
|
|
78
|
+
# fields we never captured locally (e.g. title/duration for
|
|
79
|
+
# items first added by an external client or a prior run).
|
|
80
|
+
merged = {**local_map[uid], "position": pos}
|
|
81
|
+
if not merged.get("title"):
|
|
82
|
+
merged["title"] = title
|
|
83
|
+
if not merged.get("duration_sec"):
|
|
84
|
+
merged["duration_sec"] = duration_sec
|
|
85
|
+
if not merged.get("media_id"):
|
|
86
|
+
merged["media_id"] = media_id
|
|
87
|
+
if not merged.get("media_type") or merged.get("media_type") == "unknown":
|
|
88
|
+
merged["media_type"] = media_type
|
|
69
89
|
else:
|
|
70
90
|
# New item from external source
|
|
71
91
|
merged = {
|
|
72
92
|
"uid": uid,
|
|
73
93
|
"position": pos,
|
|
74
|
-
"title":
|
|
75
|
-
"media_type":
|
|
76
|
-
"media_id":
|
|
77
|
-
"duration_sec":
|
|
94
|
+
"title": title,
|
|
95
|
+
"media_type": media_type,
|
|
96
|
+
"media_id": media_id,
|
|
97
|
+
"duration_sec": duration_sec,
|
|
78
98
|
"is_pay": False,
|
|
79
|
-
"paid_by":
|
|
99
|
+
"paid_by": queueby,
|
|
80
100
|
"tier": None,
|
|
81
101
|
"z_cost": None,
|
|
82
102
|
"schedule_id": None,
|
|
@@ -97,7 +117,8 @@ class QueueShadow:
|
|
|
97
117
|
# Start from now-playing elapsed or now
|
|
98
118
|
start_cursor = datetime.now(UTC)
|
|
99
119
|
if self._now_playing:
|
|
100
|
-
|
|
120
|
+
np_total = _to_seconds(self._now_playing.get("seconds", self._now_playing.get("duration")))
|
|
121
|
+
remaining = np_total - _to_seconds(self._now_playing.get("currentTime"))
|
|
101
122
|
start_cursor += timedelta(seconds=max(0, remaining))
|
|
102
123
|
|
|
103
124
|
for item in self._items:
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
--border: #33334a;
|
|
16
16
|
--radius: 8px;
|
|
17
17
|
--shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
18
|
+
--nav-height: 4rem;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
* {
|
|
@@ -23,6 +24,11 @@
|
|
|
23
24
|
box-sizing: border-box;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
html {
|
|
28
|
+
/* Keep anchored/scrolled-to content clear of the sticky navbar. */
|
|
29
|
+
scroll-padding-top: calc(var(--nav-height) + 1rem);
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
body {
|
|
27
33
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
28
34
|
background: var(--bg-primary);
|
|
@@ -228,6 +234,18 @@ a:hover {
|
|
|
228
234
|
}
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
/* Keep the now-playing card pinned while scrolling a long queue. */
|
|
238
|
+
.now-playing-section {
|
|
239
|
+
position: sticky;
|
|
240
|
+
top: calc(var(--nav-height) + 1rem);
|
|
241
|
+
align-self: start;
|
|
242
|
+
}
|
|
243
|
+
@media (max-width: 900px) {
|
|
244
|
+
.now-playing-section {
|
|
245
|
+
position: static;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
231
249
|
.now-playing-card {
|
|
232
250
|
background: var(--bg-card);
|
|
233
251
|
border-radius: var(--radius);
|
|
@@ -235,6 +253,9 @@ a:hover {
|
|
|
235
253
|
display: flex;
|
|
236
254
|
gap: 1rem;
|
|
237
255
|
align-items: flex-start;
|
|
256
|
+
/* Highlight the currently-playing item: subtle accent ring + glow. */
|
|
257
|
+
border: 1px solid var(--accent);
|
|
258
|
+
box-shadow: 0 0 0 1px rgba(108, 92, 231, 0.25), 0 6px 18px rgba(108, 92, 231, 0.18);
|
|
238
259
|
}
|
|
239
260
|
.np-info {
|
|
240
261
|
flex: 1;
|
|
@@ -432,6 +453,25 @@ a:hover {
|
|
|
432
453
|
.otp-sent-msg {
|
|
433
454
|
color: var(--success);
|
|
434
455
|
}
|
|
456
|
+
.auth-hint {
|
|
457
|
+
margin-top: 0.75rem;
|
|
458
|
+
padding: 0.6rem 0.85rem;
|
|
459
|
+
font-size: 0.85rem;
|
|
460
|
+
color: var(--text-secondary);
|
|
461
|
+
background: rgba(253, 203, 110, 0.1);
|
|
462
|
+
border: 1px solid var(--warning);
|
|
463
|
+
border-radius: var(--radius);
|
|
464
|
+
text-align: left;
|
|
465
|
+
}
|
|
466
|
+
.auth-hint strong {
|
|
467
|
+
color: var(--warning);
|
|
468
|
+
}
|
|
469
|
+
.auth-hint code {
|
|
470
|
+
background: var(--bg-secondary);
|
|
471
|
+
padding: 0.05rem 0.3rem;
|
|
472
|
+
border-radius: 4px;
|
|
473
|
+
font-size: 0.85em;
|
|
474
|
+
}
|
|
435
475
|
|
|
436
476
|
/* User Dashboard */
|
|
437
477
|
.user-header {
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
<div class="auth-container">
|
|
7
7
|
<h1>Login</h1>
|
|
8
8
|
<p>Enter your CyTube username to receive a one-time code via PM.</p>
|
|
9
|
+
<p class="auth-hint"><strong>Username is case-sensitive</strong> — it must match your CyTube username exactly. For example, <code>TacoBelmont</code> is not the same as <code>tacobelmont</code>.</p>
|
|
9
10
|
|
|
10
11
|
<div id="otp-request" class="auth-form">
|
|
11
|
-
<input type="text" id="username" placeholder="CyTube username" autocomplete="username" maxlength="50">
|
|
12
|
+
<input type="text" id="username" placeholder="CyTube username (case-sensitive)" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" maxlength="50">
|
|
12
13
|
<button id="request-otp-btn" class="btn btn-primary">Send Code</button>
|
|
13
14
|
</div>
|
|
14
15
|
|
|
@@ -58,6 +58,7 @@ function connectWebSocket() {
|
|
|
58
58
|
const msg = JSON.parse(event.data);
|
|
59
59
|
if (msg.type === 'queue_state' || msg.type === 'queue_update') {
|
|
60
60
|
renderQueue(msg.data);
|
|
61
|
+
maybeInitialScroll(msg.data);
|
|
61
62
|
} else if (msg.type === 'pong') {
|
|
62
63
|
// keepalive ack
|
|
63
64
|
} else if (msg.type === 'schedule_fired') {
|
|
@@ -80,8 +81,10 @@ function renderQueue(state) {
|
|
|
80
81
|
|
|
81
82
|
if (state.now_playing) {
|
|
82
83
|
const np = state.now_playing;
|
|
83
|
-
const elapsed = np.currentTime || 0;
|
|
84
|
-
|
|
84
|
+
const elapsed = Number(np.currentTime) || 0;
|
|
85
|
+
// CyTube's now-playing payload uses numeric `seconds` for the total
|
|
86
|
+
// runtime (`duration` is a preformatted "HH:MM:SS" string).
|
|
87
|
+
const total = Number(np.seconds ?? np.duration_sec ?? 0) || 0;
|
|
85
88
|
const remaining = Math.max(0, total - elapsed);
|
|
86
89
|
npEl.innerHTML = `
|
|
87
90
|
<div class="np-cover">${coverHtml(np)}</div>
|
|
@@ -131,6 +134,17 @@ function renderQueue(state) {
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// On first render with live data, bring the now-playing card into view.
|
|
138
|
+
let didInitialScroll = false;
|
|
139
|
+
function maybeInitialScroll(state) {
|
|
140
|
+
if (didInitialScroll || !state || !state.now_playing) return;
|
|
141
|
+
didInitialScroll = true;
|
|
142
|
+
const sec = document.querySelector('.now-playing-section');
|
|
143
|
+
if (sec) {
|
|
144
|
+
setTimeout(() => sec.scrollIntoView({behavior: 'smooth', block: 'start'}), 150);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
134
148
|
function formatTime(sec) {
|
|
135
149
|
sec = Math.floor(sec);
|
|
136
150
|
const h = Math.floor(sec / 3600);
|
|
@@ -169,6 +183,7 @@ async function initialLoad() {
|
|
|
169
183
|
if (resp.ok) {
|
|
170
184
|
const state = await resp.json();
|
|
171
185
|
renderQueue(state);
|
|
186
|
+
maybeInitialScroll(state);
|
|
172
187
|
}
|
|
173
188
|
} catch (e) { /* WS will take over */ }
|
|
174
189
|
}
|
|
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.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.6.2 → kryten_webqueue-0.6.3}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|