fix: warn when uploaded cookies shadow a configured cookiefile

Reported in #881, where the reporter had to reverse-engineer this from
the outside over several days: setting
YTDL_OPTIONS={"cookiefile": "/cookies/cookies.txt"} appeared to do
nothing whenever a cookies file had also been uploaded through the UI.

Uploaded cookies winning is correct and stays as it is. The upload
exists so cookies can be refreshed without restarting the container, and
letting YTDL_OPTIONS win instead would leave a visible UI button that
silently does nothing.

The defect is that it happened in silence, and could not be reported
afterwards even in principle. set_runtime_override writes into
YTDL_OPTIONS directly, so the moment an uploaded file is applied the
configured path is gone from the live config: delete_cookies' existing
has_manual_cookiefile check compares against COOKIES_PATH and therefore
cannot fire once the value has been replaced. The two override points
are the only places where both paths are still visible, so that is where
the warning has to go.

The startup path previously logged only "Cookie file detected"; both it
and the upload handler now say plainly which file is being ignored and
how to get it back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Alex Shnitman
2026-08-20 10:12:13 +02:00
parent c393e0195b
commit b74185b2af
2 changed files with 63 additions and 0 deletions
+26
View File
@@ -1069,6 +1069,30 @@ async def start(request):
COOKIES_PATH = os.path.join(config.STATE_DIR, 'cookies.txt') COOKIES_PATH = os.path.join(config.STATE_DIR, 'cookies.txt')
def warn_if_cookiefile_shadowed():
"""Warn before an uploaded cookies file displaces an operator-configured one.
Uploaded cookies deliberately win: the upload exists so cookies can be
refreshed without restarting the container, and letting YTDL_OPTIONS win
would leave a visible UI button doing nothing. But set_runtime_override
writes straight into YTDL_OPTIONS, so the configured path is gone from the
live config the moment an uploaded file is applied — after that, nothing
downstream can report the conflict (delete_cookies' has_manual_cookiefile
check cannot fire once the value has been replaced). This is the only point
where both are still visible, so it is the only place the warning can be
issued. Must be called before set_runtime_override. See issue #881, where
the silence cost the reporter days of debugging.
"""
configured = config.YTDL_OPTIONS.get('cookiefile')
if isinstance(configured, str) and configured and configured != COOKIES_PATH:
log.warning(
'Uploaded cookies at %s take precedence over the cookiefile configured in '
'YTDL_OPTIONS (%s), which will not be used. Delete the uploaded cookies from '
'the UI to go back to the configured file.',
COOKIES_PATH, configured)
@routes.post(config.URL_PREFIX + 'upload-cookies') @routes.post(config.URL_PREFIX + 'upload-cookies')
async def upload_cookies(request): async def upload_cookies(request):
reader = await request.multipart() reader = await request.multipart()
@@ -1098,6 +1122,7 @@ async def upload_cookies(request):
except OSError as exc: except OSError as exc:
log.warning(f'Could not restrict permissions on cookies file: {exc}') log.warning(f'Could not restrict permissions on cookies file: {exc}')
os.replace(tmp_cookie_path, COOKIES_PATH) os.replace(tmp_cookie_path, COOKIES_PATH)
warn_if_cookiefile_shadowed()
config.set_runtime_override('cookiefile', COOKIES_PATH) config.set_runtime_override('cookiefile', COOKIES_PATH)
log.info(f'Cookies file uploaded ({size} bytes)') log.info(f'Cookies file uploaded ({size} bytes)')
return web.Response(text=serializer.encode({'status': 'ok', 'msg': f'Cookies uploaded ({size} bytes)'})) return web.Response(text=serializer.encode({'status': 'ok', 'msg': f'Cookies uploaded ({size} bytes)'}))
@@ -1355,6 +1380,7 @@ if __name__ == '__main__':
# Auto-detect cookie file on startup # Auto-detect cookie file on startup
if os.path.exists(COOKIES_PATH): if os.path.exists(COOKIES_PATH):
warn_if_cookiefile_shadowed()
config.set_runtime_override('cookiefile', COOKIES_PATH) config.set_runtime_override('cookiefile', COOKIES_PATH)
log.info(f'Cookie file detected at {COOKIES_PATH}') log.info(f'Cookie file detected at {COOKIES_PATH}')
+37
View File
@@ -313,3 +313,40 @@ class GetCustomDirsTests(unittest.TestCase):
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
class WarnIfCookiefileShadowedTests(unittest.TestCase):
"""Issue #881: an uploaded cookies file wins over an operator-configured
cookiefile, and used to do so with no way for anyone to notice."""
def setUp(self):
self._saved = main.config.YTDL_OPTIONS
main.config.YTDL_OPTIONS = dict(self._saved)
def tearDown(self):
main.config.YTDL_OPTIONS = self._saved
def test_warns_when_a_different_cookiefile_is_configured(self):
main.config.YTDL_OPTIONS["cookiefile"] = "/cookies/cookies.txt"
with self.assertLogs("main", level="WARNING") as cm:
main.warn_if_cookiefile_shadowed()
joined = "\n".join(cm.output)
self.assertIn("/cookies/cookies.txt", joined)
self.assertIn(main.COOKIES_PATH, joined)
def test_silent_when_no_cookiefile_configured(self):
main.config.YTDL_OPTIONS.pop("cookiefile", None)
with self.assertNoLogs("main", level="WARNING"):
main.warn_if_cookiefile_shadowed()
def test_silent_when_configured_file_is_the_uploaded_one(self):
# The steady state after an upload: re-running must not nag.
main.config.YTDL_OPTIONS["cookiefile"] = main.COOKIES_PATH
with self.assertNoLogs("main", level="WARNING"):
main.warn_if_cookiefile_shadowed()
def test_silent_on_non_string_or_empty_values(self):
for value in (None, "", 0, [], {}):
main.config.YTDL_OPTIONS["cookiefile"] = value
with self.assertNoLogs("main", level="WARNING"):
main.warn_if_cookiefile_shadowed()