-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
130 lines (106 loc) · 4.33 KB
/
Copy pathconfig.py
File metadata and controls
130 lines (106 loc) · 4.33 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
"""Central configuration, loaded from environment variables (.env)."""
import os
import sys
import logging
import platform
import subprocess
from typing import Optional
from dotenv import load_dotenv
# python-dotenv is the single owner of .env — the systemd unit deliberately does
# not set EnvironmentFile, because the two parsers disagree about quoting and
# inline comments, and a value they disagree on fails silently and looks like a
# bad credential.
#
# The path is anchored to the project root rather than left to search upward
# from the working directory, so the same file is picked up whether the bot is
# started by systemd, from a shell anywhere, or by the test runner.
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
def project_path(value: Optional[str]) -> Optional[str]:
"""A path from .env, with a relative one read from the project root.
The same anchoring as .env itself, and for the same reason: resolved
against the working directory, `COOKIES_PATH=cookies.txt` silently found
nothing whenever the bot was started from anywhere else.
"""
if not value or os.path.isabs(value):
return value
return os.path.join(PROJECT_ROOT, value)
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
COMMAND_PREFIX = os.getenv("COMMAND_PREFIX", "!")
GENIUS_TOKEN = os.getenv("GENIUS_TOKEN")
COOKIES_PATH = project_path(os.getenv("COOKIES_PATH"))
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
COGS = [
"cogs.music",
"cogs.effects",
"cogs.lyrics",
"cogs.admin",
]
def configure_logging() -> None:
"""Set up structured logging. Never logs tokens or secrets."""
logging.basicConfig(
level=getattr(logging, LOG_LEVEL, logging.INFO),
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
# discord.py is chatty at INFO; keep it at WARNING unless debugging.
logging.getLogger("discord").setLevel(logging.WARNING)
def _first_line(cmd: list[str], cwd: Optional[str] = None) -> Optional[str]:
"""First line of a command's output, or None if it can't be run."""
try:
result = subprocess.run(
cmd, cwd=cwd, capture_output=True, text=True, timeout=5,
)
except (OSError, subprocess.SubprocessError):
return None
if result.returncode != 0:
return None
lines = result.stdout.strip().splitlines()
return lines[0].strip() if lines else None
def _ffmpeg_version() -> str:
# "ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 ..."
line = _first_line(["ffmpeg", "-version"])
parts = line.split() if line else []
return parts[2] if len(parts) > 2 and parts[1] == "version" else "unknown"
def runtime_versions() -> dict[str, str]:
"""
What this instance is actually running.
Every lookup degrades to "unknown" rather than failing the boot — a host
deployed without git, or without FFmpeg on PATH, should still start and say
so plainly.
"""
return {
"commit": _first_line(
["git", "rev-parse", "--short", "HEAD"], cwd=PROJECT_ROOT) or "unknown",
"yt_dlp": _first_line([sys.executable, "-m", "yt_dlp", "--version"]) or "unknown",
"ffmpeg": _ffmpeg_version(),
"python": platform.python_version(),
}
def log_runtime() -> None:
"""
Record the running versions at startup.
Without this there is no way to tell from `journalctl` which commit or which
yt-dlp was live when something broke — and a stale yt-dlp is the single most
common cause of YouTube failing.
"""
v = runtime_versions()
logging.getLogger("loopify").info(
"Running commit %s — yt-dlp %s, FFmpeg %s, Python %s",
v["commit"], v["yt_dlp"], v["ffmpeg"], v["python"],
)
def validate() -> None:
"""Fail fast with a clear message if required config is missing."""
missing = [name for name, val in {
"DISCORD_TOKEN": DISCORD_TOKEN,
}.items() if not val]
if missing:
print(
f"❌ Missing required environment variable(s): {', '.join(missing)}\n"
f" Set them in a .env file (see .env.example).",
file=sys.stderr,
)
sys.exit(1)
if not GENIUS_TOKEN:
logging.getLogger("loopify").warning(
"GENIUS_TOKEN missing — the !lyrics command will not work."
)