kryten-webqueue 0.33.3__tar.gz → 0.33.4__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.33.3 → kryten_webqueue-0.33.4}/CHANGELOG.md +12 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/PKG-INFO +1 -1
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/config.example.json +11 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/api_gate/client.py +13 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/app.py +22 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/config.py +19 -0
- kryten_webqueue-0.33.4/kryten_webqueue/jobs/rehost_emotes.py +289 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/pyproject.toml +1 -1
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/.github/workflows/gitleaks.yml +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/.gitignore +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/.gitleaks.toml +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/.pre-commit-config.yaml +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/AGENT-WORKFLOW-GUIDE.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/README.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/Kryten-Economy Service Quality Survey Report at v0.13.0.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/Kryten-Webqueue Service Quality Survey Report at v0.27.1.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/Log for incorrect promo insertion problem.txt +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/db/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/db/_catalog.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/db/_connection.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/db/_feedback.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/db/_playlists.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/db/_queue.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/completion.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/race_poller.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_feedback.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_moderation.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/feedback.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-playlists.js +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-promos.js +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-queue-mgmt.js +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-schedules.js +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin.js +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/feedback/index.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/race.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/__init__.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_feedback.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_race_poller.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_recently_played_hide.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_save_results_to_playlist.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_schedule_lock.py +0 -0
- {kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/tests/test_search_facets.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.4] - 2026-08-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `rehost_emotes` job: fetches the current channel emote list, downloads any
|
|
7
|
+
images not yet hosted on `dropsugar.co`, places them in
|
|
8
|
+
`/home/mediacms.io/mediacms/static/emotes/{bare_name}{ext}` with `www-data`
|
|
9
|
+
group ownership, updates each emote URL via api-gate, and saves timestamped
|
|
10
|
+
before/after JSON backups in `~/emote_backups/`. Runnable on demand from
|
|
11
|
+
Admin → Jobs or automatically on a configurable interval (default 24 h).
|
|
12
|
+
New config section `emote_rehost` (see `config.example.json`).
|
|
13
|
+
- `ApiGateClient.get_emotes()` and `ApiGateClient.update_emote()` methods.
|
|
14
|
+
|
|
3
15
|
All notable changes to this project will be documented in this file.
|
|
4
16
|
|
|
5
17
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
@@ -65,5 +65,16 @@
|
|
|
65
65
|
"playlist_bulk_add_delay_sec": 0.5,
|
|
66
66
|
"playlist_bulk_add_max_retries": 2,
|
|
67
67
|
|
|
68
|
+
"emote_rehost": {
|
|
69
|
+
"enabled": true,
|
|
70
|
+
"rehost_domain": "dropsugar.co",
|
|
71
|
+
"static_dir": "/home/mediacms.io/mediacms/static/emotes",
|
|
72
|
+
"base_url": "https://www.dropsugar.co/static/emotes",
|
|
73
|
+
"backup_dir": "/home/kryten/emote_backups",
|
|
74
|
+
"check_interval_hours": 24.0,
|
|
75
|
+
"download_max_retries": 5,
|
|
76
|
+
"inter_emote_delay_sec": 2.0
|
|
77
|
+
},
|
|
78
|
+
|
|
68
79
|
"prometheus_port": 28292
|
|
69
80
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import httpx
|
|
2
|
+
from urllib.parse import quote
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class ApiGateClient:
|
|
@@ -189,6 +190,18 @@ class ApiGateClient:
|
|
|
189
190
|
async def moderator_ping(self) -> dict:
|
|
190
191
|
return await self.get("/moderator/ping")
|
|
191
192
|
|
|
193
|
+
# --- Emotes ---
|
|
194
|
+
|
|
195
|
+
async def get_emotes(self) -> list[dict]:
|
|
196
|
+
result = await self.get("/emotes/")
|
|
197
|
+
return result.get("emotes", [])
|
|
198
|
+
|
|
199
|
+
async def update_emote(self, name: str, image: str) -> dict:
|
|
200
|
+
"""Update a single emote URL. name may include the '#' prefix."""
|
|
201
|
+
return await self.put(
|
|
202
|
+
f"/emotes/{quote(name, safe='')}", json={"image": image}
|
|
203
|
+
)
|
|
204
|
+
|
|
192
205
|
async def moderator_health(self) -> dict:
|
|
193
206
|
return await self.get("/moderator/health")
|
|
194
207
|
|
|
@@ -144,6 +144,14 @@ async def lifespan(app: FastAPI):
|
|
|
144
144
|
label="Fetch URLs (weekend workbook)",
|
|
145
145
|
schema=FETCHURLS_SCHEMA,
|
|
146
146
|
)
|
|
147
|
+
from .jobs.rehost_emotes import rehost_emotes_job, REHOST_EMOTES_SCHEMA
|
|
148
|
+
|
|
149
|
+
job_manager.register(
|
|
150
|
+
"rehost_emotes",
|
|
151
|
+
rehost_emotes_job,
|
|
152
|
+
label="Rehost Emotes (download & serve from dropsugar.co)",
|
|
153
|
+
schema=REHOST_EMOTES_SCHEMA,
|
|
154
|
+
)
|
|
147
155
|
app.state.job_manager = job_manager
|
|
148
156
|
|
|
149
157
|
# WebSocket manager
|
|
@@ -271,10 +279,24 @@ async def lifespan(app: FastAPI):
|
|
|
271
279
|
except Exception as e:
|
|
272
280
|
logger.error(f"Immutability expiry error: {e}")
|
|
273
281
|
|
|
282
|
+
async def _emote_rehost_loop():
|
|
283
|
+
interval_hours = config.emote_rehost.check_interval_hours
|
|
284
|
+
if not config.emote_rehost.enabled or interval_hours <= 0:
|
|
285
|
+
return
|
|
286
|
+
interval = interval_hours * 3600
|
|
287
|
+
# Mirror catalog_sync pattern: don't run immediately on startup.
|
|
288
|
+
while True:
|
|
289
|
+
await asyncio.sleep(interval)
|
|
290
|
+
try:
|
|
291
|
+
await job_manager.run("rehost_emotes", triggered_by="scheduler")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.exception("Emote rehost periodic run error: %s", e)
|
|
294
|
+
|
|
274
295
|
bg_tasks = [
|
|
275
296
|
asyncio.create_task(_catalog_sync_loop()),
|
|
276
297
|
asyncio.create_task(_otp_cleanup_loop()),
|
|
277
298
|
asyncio.create_task(_immutability_expiry_loop()),
|
|
299
|
+
asyncio.create_task(_emote_rehost_loop()),
|
|
278
300
|
]
|
|
279
301
|
|
|
280
302
|
logger.info(
|
|
@@ -3,6 +3,24 @@ from pydantic import BaseModel, PrivateAttr
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
class EmoteRehostConfig(BaseModel):
|
|
7
|
+
"""Settings for the emote rehost job."""
|
|
8
|
+
|
|
9
|
+
enabled: bool = True
|
|
10
|
+
# Emotes whose image URL contains this string are considered already rehosted.
|
|
11
|
+
rehost_domain: str = "dropsugar.co"
|
|
12
|
+
# Local directory where rehosted image files are written.
|
|
13
|
+
static_dir: str = "/home/mediacms.io/mediacms/static/emotes"
|
|
14
|
+
# Public base URL served from static_dir; final URL: {base_url}/{bare_name}{ext}
|
|
15
|
+
base_url: str = "https://www.dropsugar.co/static/emotes"
|
|
16
|
+
# Directory for timestamped backup JSON files (created if absent).
|
|
17
|
+
backup_dir: str = "/home/kryten/emote_backups"
|
|
18
|
+
# Background check interval in hours; 0 disables the periodic loop.
|
|
19
|
+
check_interval_hours: float = 24.0
|
|
20
|
+
download_max_retries: int = 5
|
|
21
|
+
inter_emote_delay_sec: float = 2.0
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
class FetchUrlsConfig(BaseModel):
|
|
7
25
|
"""Settings for the fetchurls job.
|
|
8
26
|
|
|
@@ -130,6 +148,7 @@ class Config(BaseModel):
|
|
|
130
148
|
# Jobs (optional; jobs whose config/deps are absent fail fast at run time)
|
|
131
149
|
fetch_cookies_path: str = "" # optional yt-dlp cookies for gated sources
|
|
132
150
|
fetchurls: FetchUrlsConfig = FetchUrlsConfig()
|
|
151
|
+
emote_rehost: EmoteRehostConfig = EmoteRehostConfig()
|
|
133
152
|
|
|
134
153
|
# Presence-based cancel/refund of pending paid items
|
|
135
154
|
presence_refund: PresenceRefundConfig = PresenceRefundConfig()
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Emote rehost job — download externally-hosted emotes and serve from dropsugar.co.
|
|
2
|
+
|
|
3
|
+
Processes emotes whose image URLs are not already on the configured rehost
|
|
4
|
+
domain. For each, aggressively downloads the image, places it at
|
|
5
|
+
``{static_dir}/{bare_name}{ext}`` with www-data group ownership, and pushes
|
|
6
|
+
the new URL back to CyTube via api-gate.
|
|
7
|
+
|
|
8
|
+
Emote names in the channel JSON include the ``#`` prefix (e.g. ``#behold``);
|
|
9
|
+
filenames always use the bare name without ``#`` (e.g. ``behold.gif``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import random
|
|
17
|
+
import shutil
|
|
18
|
+
import time
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from urllib.parse import unquote, urlparse
|
|
22
|
+
|
|
23
|
+
import requests
|
|
24
|
+
from requests.adapters import HTTPAdapter
|
|
25
|
+
from urllib3.util.retry import Retry
|
|
26
|
+
|
|
27
|
+
from .manager import JobError
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
REHOST_EMOTES_SCHEMA: list[dict] = []
|
|
32
|
+
|
|
33
|
+
_USER_AGENTS = [
|
|
34
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
35
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
36
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
37
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0 Safari/537.36",
|
|
38
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _detect_ext(url: str, content_type: str | None) -> str:
|
|
43
|
+
"""Determine file extension from Content-Type or URL path; default .gif."""
|
|
44
|
+
if content_type:
|
|
45
|
+
ct = content_type.lower()
|
|
46
|
+
if "gif" in ct:
|
|
47
|
+
return ".gif"
|
|
48
|
+
if "png" in ct:
|
|
49
|
+
return ".png"
|
|
50
|
+
if "jpeg" in ct or "jpg" in ct:
|
|
51
|
+
return ".jpg"
|
|
52
|
+
if "webp" in ct:
|
|
53
|
+
return ".webp"
|
|
54
|
+
path = unquote(urlparse(url).path).lower()
|
|
55
|
+
for ext in (".gif", ".png", ".jpg", ".jpeg", ".webp"):
|
|
56
|
+
if path.endswith(ext):
|
|
57
|
+
return ".jpg" if ext == ".jpeg" else ext
|
|
58
|
+
return ".gif"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _make_session() -> requests.Session:
|
|
62
|
+
session = requests.Session()
|
|
63
|
+
retry = Retry(
|
|
64
|
+
total=5,
|
|
65
|
+
backoff_factor=2,
|
|
66
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
67
|
+
allowed_methods=["HEAD", "GET"],
|
|
68
|
+
)
|
|
69
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
70
|
+
session.mount("https://", adapter)
|
|
71
|
+
session.mount("http://", adapter)
|
|
72
|
+
return session
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _set_permissions(path: Path) -> None:
|
|
76
|
+
"""Set mode 0644 and group to www-data (best-effort; logs on failure)."""
|
|
77
|
+
path.chmod(0o644)
|
|
78
|
+
try:
|
|
79
|
+
import grp # type: ignore[import] # Unix only
|
|
80
|
+
|
|
81
|
+
gid = grp.getgrnam("www-data").gr_gid # type: ignore[attr-defined]
|
|
82
|
+
os.chown(path, -1, gid) # type: ignore[attr-defined]
|
|
83
|
+
except (ImportError, KeyError, PermissionError) as exc:
|
|
84
|
+
logger.warning("Could not set www-data group on %s: %s", path, exc)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _place_emote(
|
|
88
|
+
url: str, bare_name: str, static_dir: Path, max_retries: int
|
|
89
|
+
) -> str | None:
|
|
90
|
+
"""Download url → static_dir/{bare_name}{ext} with correct permissions.
|
|
91
|
+
|
|
92
|
+
Returns the file extension (e.g. ``.gif``) on success, ``None`` if all
|
|
93
|
+
strategies are exhausted. Blocking — must be called via asyncio.to_thread.
|
|
94
|
+
"""
|
|
95
|
+
base = static_dir / bare_name
|
|
96
|
+
tmp = base.parent / f"{base.name}.tmp"
|
|
97
|
+
session = _make_session()
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Primary strategy: resumable, rotating User-Agent, progressive timeout
|
|
101
|
+
for attempt in range(max_retries):
|
|
102
|
+
try:
|
|
103
|
+
headers: dict[str, str] = {
|
|
104
|
+
"User-Agent": random.choice(_USER_AGENTS),
|
|
105
|
+
"Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
|
|
106
|
+
"DNT": "1",
|
|
107
|
+
}
|
|
108
|
+
if tmp.exists():
|
|
109
|
+
headers["Range"] = f"bytes={tmp.stat().st_size}-"
|
|
110
|
+
|
|
111
|
+
timeout = min(60 + attempt * 30, 180)
|
|
112
|
+
resp = session.get(url, headers=headers, timeout=timeout, stream=True)
|
|
113
|
+
resp.raise_for_status()
|
|
114
|
+
|
|
115
|
+
ext = _detect_ext(url, resp.headers.get("content-type"))
|
|
116
|
+
mode = "ab" if "Range" in headers and resp.status_code == 206 else "wb"
|
|
117
|
+
with open(tmp, mode) as fh:
|
|
118
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
119
|
+
if chunk:
|
|
120
|
+
fh.write(chunk)
|
|
121
|
+
|
|
122
|
+
final = base.with_suffix(ext)
|
|
123
|
+
shutil.move(str(tmp), str(final))
|
|
124
|
+
_set_permissions(final)
|
|
125
|
+
return ext
|
|
126
|
+
|
|
127
|
+
except requests.exceptions.RequestException as exc:
|
|
128
|
+
logger.debug(
|
|
129
|
+
"Attempt %d/%d for %s: %s", attempt + 1, max_retries, url, exc
|
|
130
|
+
)
|
|
131
|
+
time.sleep(2**attempt + random.uniform(0, 1))
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.debug(
|
|
134
|
+
"Unexpected error attempt %d/%d for %s: %s",
|
|
135
|
+
attempt + 1,
|
|
136
|
+
max_retries,
|
|
137
|
+
url,
|
|
138
|
+
exc,
|
|
139
|
+
)
|
|
140
|
+
tmp.unlink(missing_ok=True)
|
|
141
|
+
time.sleep(2**attempt + random.uniform(0, 1))
|
|
142
|
+
|
|
143
|
+
# Fallback: minimal headers, non-streaming
|
|
144
|
+
for attempt in range(2):
|
|
145
|
+
try:
|
|
146
|
+
resp = requests.get(
|
|
147
|
+
url,
|
|
148
|
+
headers={"User-Agent": "curl/7.68.0", "Accept": "*/*"},
|
|
149
|
+
timeout=120,
|
|
150
|
+
)
|
|
151
|
+
resp.raise_for_status()
|
|
152
|
+
ext = _detect_ext(url, resp.headers.get("content-type"))
|
|
153
|
+
final = base.with_suffix(ext)
|
|
154
|
+
final.write_bytes(resp.content)
|
|
155
|
+
_set_permissions(final)
|
|
156
|
+
return ext
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.debug("Fallback attempt %d for %s: %s", attempt + 1, url, exc)
|
|
159
|
+
time.sleep(2)
|
|
160
|
+
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
finally:
|
|
164
|
+
session.close()
|
|
165
|
+
tmp.unlink(missing_ok=True)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _write_json(path: Path, data: list[dict]) -> None:
|
|
169
|
+
"""Atomically write data to path as indented JSON."""
|
|
170
|
+
tmp = path.with_suffix(".tmp")
|
|
171
|
+
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
172
|
+
tmp.replace(path)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def rehost_emotes_job(params: dict, ctx) -> dict:
|
|
176
|
+
"""Rehost externally-hosted channel emotes to dropsugar.co."""
|
|
177
|
+
cfg = ctx.config.emote_rehost
|
|
178
|
+
api = ctx.api_gate
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
emotes = await api.get_emotes()
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
raise JobError(f"Failed to fetch emotes from api-gate: {exc}") from exc
|
|
184
|
+
|
|
185
|
+
if not emotes:
|
|
186
|
+
return {
|
|
187
|
+
"total_emotes": 0,
|
|
188
|
+
"already_rehosted": 0,
|
|
189
|
+
"attempted": 0,
|
|
190
|
+
"succeeded": 0,
|
|
191
|
+
"failed": 0,
|
|
192
|
+
"pushed": 0,
|
|
193
|
+
"failed_emotes": [],
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Back up current state before making any changes
|
|
197
|
+
backup_dir = Path(cfg.backup_dir)
|
|
198
|
+
await asyncio.to_thread(backup_dir.mkdir, parents=True, exist_ok=True)
|
|
199
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
200
|
+
await asyncio.to_thread(
|
|
201
|
+
_write_json, backup_dir / f"emotes-{stamp}-before.json", emotes
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
to_rehost = [e for e in emotes if cfg.rehost_domain not in e.get("image", "")]
|
|
205
|
+
already = len(emotes) - len(to_rehost)
|
|
206
|
+
await ctx.progress(
|
|
207
|
+
{"step": "fetched", "total": len(emotes), "to_rehost": len(to_rehost)}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if not to_rehost:
|
|
211
|
+
result: dict = {
|
|
212
|
+
"total_emotes": len(emotes),
|
|
213
|
+
"already_rehosted": already,
|
|
214
|
+
"attempted": 0,
|
|
215
|
+
"succeeded": 0,
|
|
216
|
+
"failed": 0,
|
|
217
|
+
"pushed": 0,
|
|
218
|
+
"failed_emotes": [],
|
|
219
|
+
}
|
|
220
|
+
await ctx.progress({"step": "complete", **result})
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
static_dir = Path(cfg.static_dir)
|
|
224
|
+
await asyncio.to_thread(static_dir.mkdir, parents=True, exist_ok=True)
|
|
225
|
+
|
|
226
|
+
succeeded: list[str] = []
|
|
227
|
+
failed: list[str] = []
|
|
228
|
+
# mutable working copy keyed by name for building the after-backup
|
|
229
|
+
updated = {e["name"]: dict(e) for e in emotes}
|
|
230
|
+
|
|
231
|
+
for i, emote in enumerate(to_rehost, 1):
|
|
232
|
+
name = emote["name"] # e.g. "#behold" (includes #)
|
|
233
|
+
url = emote["image"]
|
|
234
|
+
bare = name.lstrip("#") # "behold" → used as filename root
|
|
235
|
+
|
|
236
|
+
ext = await asyncio.to_thread(
|
|
237
|
+
_place_emote, url, bare, static_dir, cfg.download_max_retries
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if ext is None:
|
|
241
|
+
logger.warning(
|
|
242
|
+
"All download strategies failed for emote %s (%s)", name, url
|
|
243
|
+
)
|
|
244
|
+
failed.append(name)
|
|
245
|
+
await ctx.progress(
|
|
246
|
+
{"step": "failed", "emote": name, "done": i, "total": len(to_rehost)}
|
|
247
|
+
)
|
|
248
|
+
await asyncio.sleep(cfg.inter_emote_delay_sec)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
new_url = f"{cfg.base_url.rstrip('/')}/{bare}{ext}"
|
|
252
|
+
updated[name]["image"] = new_url
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
await api.update_emote(name, new_url)
|
|
256
|
+
succeeded.append(name)
|
|
257
|
+
await ctx.progress(
|
|
258
|
+
{"step": "pushed", "emote": name, "done": i, "total": len(to_rehost)}
|
|
259
|
+
)
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
logger.error("Failed to push emote %s to api-gate: %s", name, exc)
|
|
262
|
+
failed.append(name)
|
|
263
|
+
await ctx.progress(
|
|
264
|
+
{
|
|
265
|
+
"step": "push_failed",
|
|
266
|
+
"emote": name,
|
|
267
|
+
"done": i,
|
|
268
|
+
"total": len(to_rehost),
|
|
269
|
+
"error": str(exc),
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
await asyncio.sleep(cfg.inter_emote_delay_sec)
|
|
274
|
+
|
|
275
|
+
await asyncio.to_thread(
|
|
276
|
+
_write_json, backup_dir / f"emotes-{stamp}-after.json", list(updated.values())
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
result = {
|
|
280
|
+
"total_emotes": len(emotes),
|
|
281
|
+
"already_rehosted": already,
|
|
282
|
+
"attempted": len(to_rehost),
|
|
283
|
+
"succeeded": len(succeeded),
|
|
284
|
+
"failed": len(failed),
|
|
285
|
+
"pushed": len(succeeded),
|
|
286
|
+
"failed_emotes": failed,
|
|
287
|
+
}
|
|
288
|
+
await ctx.progress({"step": "complete", **result})
|
|
289
|
+
return result
|
|
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.33.3 → kryten_webqueue-0.33.4}/docs/Log for incorrect promo insertion problem.txt
RENAMED
|
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.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
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.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/routes/admin_moderation.py
RENAMED
|
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.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-playlists.js
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-queue-mgmt.js
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/static/js/admin-schedules.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/feedback/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.33.3 → kryten_webqueue-0.33.4}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
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
|