Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3be9556eb4
|
|||
|
e952c8161a
|
|||
|
8d665a7f34
|
|||
|
6e4f8c06b4
|
|||
|
d7c05fd058
|
|||
| 8cd122d50d | |||
| f531393481 | |||
| ce43b34ce0 | |||
| ddb755e7e5 | |||
| 682a5b949f | |||
| 6c72acc59e | |||
| b381c7b012 | |||
| a3c12855fe | |||
| bbd374590e | |||
| fd72b415c5 | |||
| 97f6aeb538 |
@@ -22,6 +22,9 @@ downtify/Downtify_downloads
|
|||||||
headscale/config/*
|
headscale/config/*
|
||||||
headscale/data/*
|
headscale/data/*
|
||||||
|
|
||||||
|
# Steaming services files
|
||||||
|
streaming/config/*
|
||||||
|
|
||||||
# Steaming services files
|
# Steaming services files
|
||||||
streaming/jellyfin/*
|
streaming/jellyfin/*
|
||||||
streaming/jellyseerr/*
|
streaming/jellyseerr/*
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Suppress HTTP request logs
|
||||||
|
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('httpx').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('telegram.ext._application').setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
def _env(key, default=None):
|
def _env(key, default=None):
|
||||||
v = os.getenv(key, default)
|
v = os.getenv(key, default)
|
||||||
@@ -39,7 +44,7 @@ KEY_WHITELIST = "bot:whitelist"
|
|||||||
KEY_WHITELIST_ENABLED = "bot:whitelist_enabled"
|
KEY_WHITELIST_ENABLED = "bot:whitelist_enabled"
|
||||||
KEY_SUBSCRIBERS = "bot:subscribers"
|
KEY_SUBSCRIBERS = "bot:subscribers"
|
||||||
KEY_PHPSESSID = "EDU_PHPSESSID"
|
KEY_PHPSESSID = "EDU_PHPSESSID"
|
||||||
KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 5 webinars
|
KEY_WEBINAR_HISTORY = "bot:webinar_history" # Stores last 3 webinars
|
||||||
|
|
||||||
# Initialize Redis
|
# Initialize Redis
|
||||||
try:
|
try:
|
||||||
@@ -89,6 +94,8 @@ TRANSLATIONS = {
|
|||||||
'flag_ru': "🇷🇺 Русский",
|
'flag_ru': "🇷🇺 Русский",
|
||||||
'flag_uk': "🇺🇦 Українська",
|
'flag_uk': "🇺🇦 Українська",
|
||||||
'flag_en': "🇬🇧 English",
|
'flag_en': "🇬🇧 English",
|
||||||
|
'history_cleared': "✅ История вебинаров очищена",
|
||||||
|
'history_clear_failed': "❌ Ошибка при очистке истории",
|
||||||
},
|
},
|
||||||
'uk': {
|
'uk': {
|
||||||
'welcome': "👋 Привіт, {name}!\n\nЯ бот-сповіщувач про вебінари. Я повідомлятиму вас, коли з'явиться новий вебінар.\nВи підписані на сповіщення.",
|
'welcome': "👋 Привіт, {name}!\n\nЯ бот-сповіщувач про вебінари. Я повідомлятиму вас, коли з'явиться новий вебінар.\nВи підписані на сповіщення.",
|
||||||
@@ -126,6 +133,8 @@ TRANSLATIONS = {
|
|||||||
'flag_ru': "🇷🇺 Русский",
|
'flag_ru': "🇷🇺 Русский",
|
||||||
'flag_uk': "🇺🇦 Українська",
|
'flag_uk': "🇺🇦 Українська",
|
||||||
'flag_en': "🇬🇧 English",
|
'flag_en': "🇬🇧 English",
|
||||||
|
'history_cleared': "✅ Історія вебінарів очищена",
|
||||||
|
'history_clear_failed': "❌ Помилка при очищенні історії",
|
||||||
},
|
},
|
||||||
'en': {
|
'en': {
|
||||||
'welcome': "👋 Hello, {name}!\n\nI am the Webinar Checker Bot. I will notify you when a new webinar appears.\nYou have been subscribed to notifications.",
|
'welcome': "👋 Hello, {name}!\n\nI am the Webinar Checker Bot. I will notify you when a new webinar appears.\nYou have been subscribed to notifications.",
|
||||||
@@ -163,6 +172,8 @@ TRANSLATIONS = {
|
|||||||
'flag_ru': "🇷🇺 Русский",
|
'flag_ru': "🇷🇺 Русский",
|
||||||
'flag_uk': "🇺🇦 Українська",
|
'flag_uk': "🇺🇦 Українська",
|
||||||
'flag_en': "🇬🇧 English",
|
'flag_en': "🇬🇧 English",
|
||||||
|
'history_cleared': "✅ Webinar history cleared",
|
||||||
|
'history_clear_failed': "❌ Error clearing history",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,6 +361,21 @@ async def remove_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
|
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
|
||||||
|
|
||||||
|
async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Clear webinar history (admin only)."""
|
||||||
|
admin_id = update.effective_user.id
|
||||||
|
if admin_id != ADMIN_ID:
|
||||||
|
await update.message.reply_text(t(admin_id, 'admin_only'))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
redis_client.delete(KEY_WEBINAR_HISTORY)
|
||||||
|
await update.message.reply_text(t(admin_id, 'history_cleared'))
|
||||||
|
logger.info("Admin cleared webinar history")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to clear history: {e}")
|
||||||
|
await update.message.reply_text(t(admin_id, 'history_clear_failed'))
|
||||||
|
|
||||||
# --- Admin Callbacks ---
|
# --- Admin Callbacks ---
|
||||||
|
|
||||||
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
@@ -410,9 +436,9 @@ async def language_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
# --- Webinar Checking Job ---
|
# --- Webinar Checking Job ---
|
||||||
|
|
||||||
def get_webinar_key(name: str, url: str) -> str:
|
def get_webinar_key(url: str) -> str:
|
||||||
"""Generate unique key for a webinar based on name and URL."""
|
"""Generate unique key for a webinar based on URL."""
|
||||||
return f"{name}|{url}"
|
return url
|
||||||
|
|
||||||
def get_stored_webinars() -> list:
|
def get_stored_webinars() -> list:
|
||||||
"""Get list of stored webinar keys from Redis."""
|
"""Get list of stored webinar keys from Redis."""
|
||||||
@@ -425,9 +451,9 @@ def get_stored_webinars() -> list:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def store_webinars(webinar_keys: list):
|
def store_webinars(webinar_keys: list):
|
||||||
"""Store up to 5 most recent webinar keys in Redis."""
|
"""Store up to 3 most recent webinar keys in Redis."""
|
||||||
# Keep only last 5
|
# Keep only last 3
|
||||||
webinar_keys = webinar_keys[-5:]
|
webinar_keys = webinar_keys[-3:]
|
||||||
try:
|
try:
|
||||||
redis_client.set(KEY_WEBINAR_HISTORY, json.dumps(webinar_keys))
|
redis_client.set(KEY_WEBINAR_HISTORY, json.dumps(webinar_keys))
|
||||||
logger.info(f"Stored {len(webinar_keys)} webinar(s) in history")
|
logger.info(f"Stored {len(webinar_keys)} webinar(s) in history")
|
||||||
@@ -562,7 +588,7 @@ async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
|
|||||||
current_keys = []
|
current_keys = []
|
||||||
|
|
||||||
for webinar in current_webinars:
|
for webinar in current_webinars:
|
||||||
key = get_webinar_key(webinar['name'], webinar['url'])
|
key = get_webinar_key(webinar['url'])
|
||||||
current_keys.append(key)
|
current_keys.append(key)
|
||||||
|
|
||||||
if key not in stored_keys:
|
if key not in stored_keys:
|
||||||
@@ -622,6 +648,7 @@ def main():
|
|||||||
app.add_handler(CommandHandler("language", language_command))
|
app.add_handler(CommandHandler("language", language_command))
|
||||||
app.add_handler(CommandHandler("adduser", add_user))
|
app.add_handler(CommandHandler("adduser", add_user))
|
||||||
app.add_handler(CommandHandler("removeuser", remove_user))
|
app.add_handler(CommandHandler("removeuser", remove_user))
|
||||||
|
app.add_handler(CommandHandler("clearhistory", clear_history))
|
||||||
|
|
||||||
# Callback handlers - language selection first, then admin panel
|
# Callback handlers - language selection first, then admin panel
|
||||||
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
|
app.add_handler(CallbackQueryHandler(language_callback, pattern="^lang_"))
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
TZ=Europe/Moscow
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
services:
|
||||||
|
jellyfin:
|
||||||
|
image: lscr.io/linuxserver/jellyfin:latest
|
||||||
|
container_name: jellyfin
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8096:8096/tcp"
|
||||||
|
- "7359:7359/udp"
|
||||||
|
volumes:
|
||||||
|
# HACK: Binding while bootstrapping, testing
|
||||||
|
# - jellyfin-cfg:/config
|
||||||
|
- ./config/jellyfin:/config
|
||||||
|
- /mnt/mediaserver/media/movies:/media/movies
|
||||||
|
- /mnt/mediaserver/media/movies:/media/movies
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
|
||||||
|
# Prod Router
|
||||||
|
- "traefik.http.routers.jellyfin.rule=Host(`media.forust.xyz`)"
|
||||||
|
- "traefik.http.routers.jellyfin.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.jellyfin.tls=true"
|
||||||
|
# Local Router
|
||||||
|
- "traefik.http.routers.jellyfin-local.rule=Host(`media.workstation.internal`) || Host(`ms.internal`)"
|
||||||
|
- "traefik.http.routers.jellyfin-local.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.jellyfin-local.tls=true"
|
||||||
|
# Dev Router
|
||||||
|
- "traefik.http.routers.jellyfin-dev.rule=Host(`media.gigaforust.internal`)"
|
||||||
|
- "traefik.http.routers.jellyfin-dev.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.jellyfin-dev.tls=true"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- streaming
|
||||||
|
qbittorrent:
|
||||||
|
image: lscr.io/linuxserver/qbittorrent:latest
|
||||||
|
container_name: qbittorrent
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- WEBUI_PORT=8080
|
||||||
|
volumes:
|
||||||
|
# HACK: Binding while bootstrapping, testing
|
||||||
|
# - qbittorrent-cfg:/config
|
||||||
|
- ./config/qbittorrent:/config
|
||||||
|
- /mnt/mediaserver/downloads:/downloads
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
- 6881:6881
|
||||||
|
- 6881:6881/udp
|
||||||
|
networks:
|
||||||
|
- streaming
|
||||||
|
radarr:
|
||||||
|
image: lscr.io/linuxserver/radarr:latest
|
||||||
|
container_name: radarr
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 7878:7878
|
||||||
|
volumes:
|
||||||
|
# HACK: Binding while bootstrapping, testing
|
||||||
|
# - radarr-cfg:/config
|
||||||
|
- ./config/radarr:/config
|
||||||
|
- /mnt/mediaserver/downloads:/downloads
|
||||||
|
- /mnt/mediaserver/movies:/movies
|
||||||
|
networks:
|
||||||
|
- streaming
|
||||||
|
volumes:
|
||||||
|
jellyfin-cfg:
|
||||||
|
qbittorrent-cfg:
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
streaming:
|
||||||
|
name: streaming
|
||||||
Reference in New Issue
Block a user