Add subtitle downloader web application with OpenSubtitles API integration#1075
Add subtitle downloader web application with OpenSubtitles API integration#1075
Conversation
Creates a complete Flask-based web application for downloading TV show subtitles from OpenSubtitles.com. The application allows users to: - Search for subtitles by series name, season/episode, and language - View available subtitles with metadata (release name, downloads, upload date) - Download SRT files directly to their machine Key components: - Backend: Flask web framework with OpenSubtitles API integration - Frontend: Responsive HTML/CSS/JavaScript interface - API Layer: OpenSubtitlesHandler class for API communication - Utils: Input validation and helper functions - Configuration: Environment-based settings for multiple deployment modes Project structure: - app.py: Main Flask application with routes - config.py: Configuration management (languages, API settings) - subtitle_handler/: Module for subtitle operations - opensubtitles.py: OpenSubtitles API wrapper - utils.py: Input validation and parsing utilities - templates/index.html: Web UI template - static/css/style.css: Responsive styling with gradient background - static/js/script.js: Frontend logic for search and download - requirements.txt: Python dependencies Tested: - Python syntax validation for all modules - Configuration loading - Input validation functions - Season/episode parsing https://claude.ai/code/session_01Xg36Q4fJoN1hKHsbpYbNCS
📝 WalkthroughWalkthroughA new Flask-based web application for subtitle downloading is introduced, featuring API endpoints for searching and downloading subtitles via OpenSubtitles integration, a configuration system with environment-based settings, and a complete frontend UI with search, results display, and download functionality. Changes
Sequence DiagramssequenceDiagram
participant Client as Browser Client
participant Server as Flask App
participant Handler as OpenSubtitlesHandler
participant API as OpenSubtitles API
Client->>Server: POST /api/search (series_name, season_episode, language)
Server->>Server: validate_input()
Server->>Server: get_language_code()
Server->>Handler: search_subtitles(name, episode, code)
Handler->>Handler: parse_season_episode()
Handler->>API: GET /search (query parameters)
API-->>Handler: JSON response (subtitle list)
Handler->>Handler: Parse and structure results
Handler-->>Server: List of subtitles
Server-->>Client: JSON with results or empty set
Client->>Client: Display results or no-results message
sequenceDiagram
participant Client as Browser Client
participant Server as Flask App
participant Handler as OpenSubtitlesHandler
participant API as OpenSubtitles API
participant FS as Filesystem
Client->>Server: GET /api/download/<subtitle_id>
Server->>Handler: download_subtitle(subtitle_id)
Handler->>API: GET /subtitles/{id}/download
API-->>Handler: Redirect URL + metadata
Handler->>API: GET subtitle file (via redirect)
API-->>Handler: File content (binary)
Handler-->>Server: (content, file_name)
Server->>Server: Create BytesIO object
Server-->>Client: File as attachment (send_file)
Client->>Client: Browser downloads file
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can generate a title for your PR based on the changes.Add |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (5)
subtitle-downloader/.gitignore (2)
9-39: Remove duplicatedownloads/ignore entry.
downloads/is listed at both Line 9 and Line 39. Keep a single entry to reduce config noise.Proposed cleanup
@@ -downloads/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subtitle-downloader/.gitignore` around lines 9 - 39, Remove the duplicated ignore entry "downloads/" in the .gitignore content: locate both occurrences of the literal "downloads/" entry and delete one so only a single "downloads/" line remains; ensure file still contains the other ignore patterns unchanged and keep exactly one "downloads/" entry to avoid redundant entries.
30-30: Broaden env-file ignore patterns to reduce secret leakage risk.Line 30 ignores
.env, but variant files (for example.env.local/.env.production) can still be committed accidentally.Proposed hardening
.env +.env.* +!.env.example🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subtitle-downloader/.gitignore` at line 30, The .gitignore currently only ignores `.env` which still allows variants like `.env.local` to be committed; update the ignore rules in .gitignore by replacing or augmenting the `.env` entry with a broader pattern such as `.env*` (to cover `.env.local`, `.env.production`, etc.) and add an allow-list entry `!.env.example` if you want to keep an example file tracked; adjust the `.env` line accordingly in .gitignore.subtitle-downloader/templates/index.html (1)
45-67: Language options are hardcoded and duplicated fromconfig.py.The language dropdown duplicates the
Config.LANGUAGESdictionary. If languages are added/removed inconfig.py, this HTML must be manually updated. Consider dynamically populating the dropdown via the/api/languagesendpoint on page load, or use Jinja2 templating to generate options from the config.♻️ Option 1: Use Jinja2 to render from config
In
app.py, pass languages to the template:`@app.route`('/') def index(): return render_template('index.html', languages=Config.LANGUAGES)In
index.html:<select id="language" name="language" required> <option value="">Select Language</option> {% for name, code in languages.items() %} <option value="{{ name }}">{{ name }}</option> {% endfor %} </select>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subtitle-downloader/templates/index.html` around lines 45 - 67, The language select in index.html is hardcoded and duplicates Config.LANGUAGES; update the template to populate options dynamically instead of static <select> entries by either (a) passing Config.LANGUAGES from app.py's index() route into render_template() and iterating over it in index.html to emit <option>s (reference: Config.LANGUAGES and index() / render_template), or (b) fetching /api/languages on page load and filling the element with id="language" via JavaScript; remove the hardcoded <option> lines and ensure the select keeps id="language" and name="language" and the empty "Select Language" placeholder option remains.subtitle-downloader/subtitle_handler/opensubtitles.py (1)
118-122: Movereimport to module level.The
remodule is imported inside the function. For consistency and slight performance benefit, move it to the top with other imports.♻️ Proposed fix
At the top of the file (after line 4):
import reThen remove line 119:
if 'content-disposition' in download_response.headers: - import re match = re.search(r'filename="?(.+?)"?(?:;|$)', download_response.headers['content-disposition'])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subtitle-downloader/subtitle_handler/opensubtitles.py` around lines 118 - 122, Move the inline import of the regular expressions module out of the function and into the module-level imports: remove the local "import re" inside the block that checks download_response.headers and add "import re" at the top of the file alongside the other imports in opensubtitles.py; ensure the code that uses re.search (the block referencing download_response.headers and file_name) continues to work unchanged after removing the local import.subtitle-downloader/app.py (1)
127-137: Keep browser error responses HTML.The app serves
index.html, but Lines 127-137 force JSON for every 404/500. Missing pages or template/static failures in the browser will render as raw API payloads instead of a usable error page. Consider returning JSON only for/api/*and rendering HTML for the UI routes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@subtitle-downloader/app.py` around lines 127 - 137, The not_found and server_error handlers currently always return JSON; change them to return JSON only for API requests and return the UI HTML for browser routes: inside not_found(error) and server_error(error) detect API calls (e.g., if request.path.startswith('/api/') or request.accept_mimetypes.accept_json) and return jsonify with the appropriate status for those cases, otherwise render or send your frontend index.html (using render_template('index.html') or send_from_directory) with the same 404/500 status; keep the logger.error(...) in server_error and ensure you import request and render_template/send_from_directory as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@subtitle-downloader/app.py`:
- Around line 18-19: Current code indexes config with config[env] which will
raise a KeyError on typos/unexpected FLASK_ENV values; change to an explicit
lookup: check whether env is in the config registry (e.g., if env not in config)
and either raise a clear startup error (RuntimeError with a message including
the invalid env and allowed keys) or assign a deliberate fallback (e.g.,
fallback = 'development') before calling app.config.from_object; use the env
variable name and app.config.from_object(config_lookup) to locate the change
points.
- Around line 145-148: Change the dev server bind in the app.run call so it
defaults to localhost instead of all interfaces: replace the hardcoded '0.0.0.0'
host in the app.run(...) invocation with a configurable value that defaults to
'127.0.0.1' (e.g., read from an environment variable such as FLASK_RUN_HOST or a
custom HOST env var) while keeping the existing app.config['DEBUG'] usage;
update the app.run call site so it uses that env-derived host to allow explicit
wider binding when needed.
- Around line 43-52: Change request.get_json() to request.get_json(silent=True)
and validate before calling .strip(): ensure the returned data is a dict (return
jsonify error 400 if not), then for each field (series_name, season_episode,
language) fetch raw = data.get('field') and if raw is None or not
isinstance(raw, str) return a 400 JSON error; only after those checks call
raw.strip() into series_name/season_episode/language. Also adjust the broad
exception handling so that client-side issues (malformed JSON, type errors,
missing fields) return 400 rather than being swallowed as 500—e.g., explicitly
return 400 for known client errors and let unexpected exceptions propagate to
the existing server-error handler.
In `@subtitle-downloader/config.py`:
- Line 5: The config currently sets SECRET_KEY = os.environ.get('SECRET_KEY') or
'dev-secret-key-change-in-production', which provides a dangerous production
fallback; remove the hardcoded fallback and make ProductionConfig require the
env var explicitly (e.g. read via os.environ['SECRET_KEY'] or validate and raise
RuntimeError/AssertionError if SECRET_KEY is missing) so that the app fails to
start when SECRET_KEY is not provided; update the SECRET_KEY declaration and/or
override it in the ProductionConfig class (referencing SECRET_KEY and
ProductionConfig) to enforce this check and include a clear error message.
In `@subtitle-downloader/requirements.txt`:
- Line 1: Update the Flask dependency entry in requirements.txt from
"Flask==2.3.0" to a patched version; change the line to "Flask==2.3.2" (minimum
remediation) or "Flask==3.1.3" for full coverage of both CVEs, then re-run any
lock/installation steps (e.g., pip install -r requirements.txt or your
dependency lock generation) to ensure the updated package is applied; locate the
single dependency line "Flask==2.3.0" in requirements.txt and replace it
accordingly.
In `@subtitle-downloader/static/css/style.css`:
- Line 239: Replace the deprecated CSS declaration using "word-break:
break-word" by removing that property and adding "overflow-wrap: break-word"
(and optionally keep "word-break: normal" for explicit fallback) wherever
"word-break: break-word" appears in the stylesheet so the rule is updated to use
the modern, better-supported overflow-wrap behavior.
In `@subtitle-downloader/static/js/script.js`:
- Around line 156-161: The condition incorrectly checks subtitle.download_count
while the API returns subtitle.downloads, so update the check in the block that
creates downloadSpan to use subtitle.downloads (e.g., change the condition from
subtitle.download_count !== undefined to subtitle.downloads !== undefined or use
a truthy check) and ensure the created downloadSpan still uses
subtitle.downloads (and falls back to 0) before appending it to metaDiv.
In `@subtitle-downloader/subtitle_handler/opensubtitles.py`:
- Around line 154-162: get_supported_languages currently returns a dict mapping
language codes to names (languages variable) but on exception returns
Config.LANGUAGES which is the inverse mapping (name->code); change the exception
return to transform Config.LANGUAGES into the same {code: name} shape before
returning (e.g., iterate Config.LANGUAGES items and build {code: name}) so the
API contract is preserved; update the except block that logs via logger.error to
return the transformed mapping instead of Config.LANGUAGES directly.
In `@subtitle-downloader/subtitle_handler/utils.py`:
- Around line 16-17: The docstring for the function in subtitle_handler.utils
that documents a return of {'valid': bool, 'error': str or None} is out of sync
with the implementation which returns {'valid': bool, 'errors': list}; update
the docstring to describe the actual return shape (e.g., {'valid': bool,
'errors': list[str]} and what items in the list mean) or alternatively change
the function to return a single 'error' string to match the current
docstring—locate the function by the return statement that constructs {'valid':
..., 'errors': ...} and make the docstring and implementation consistent.
---
Nitpick comments:
In `@subtitle-downloader/.gitignore`:
- Around line 9-39: Remove the duplicated ignore entry "downloads/" in the
.gitignore content: locate both occurrences of the literal "downloads/" entry
and delete one so only a single "downloads/" line remains; ensure file still
contains the other ignore patterns unchanged and keep exactly one "downloads/"
entry to avoid redundant entries.
- Line 30: The .gitignore currently only ignores `.env` which still allows
variants like `.env.local` to be committed; update the ignore rules in
.gitignore by replacing or augmenting the `.env` entry with a broader pattern
such as `.env*` (to cover `.env.local`, `.env.production`, etc.) and add an
allow-list entry `!.env.example` if you want to keep an example file tracked;
adjust the `.env` line accordingly in .gitignore.
In `@subtitle-downloader/app.py`:
- Around line 127-137: The not_found and server_error handlers currently always
return JSON; change them to return JSON only for API requests and return the UI
HTML for browser routes: inside not_found(error) and server_error(error) detect
API calls (e.g., if request.path.startswith('/api/') or
request.accept_mimetypes.accept_json) and return jsonify with the appropriate
status for those cases, otherwise render or send your frontend index.html (using
render_template('index.html') or send_from_directory) with the same 404/500
status; keep the logger.error(...) in server_error and ensure you import request
and render_template/send_from_directory as needed.
In `@subtitle-downloader/subtitle_handler/opensubtitles.py`:
- Around line 118-122: Move the inline import of the regular expressions module
out of the function and into the module-level imports: remove the local "import
re" inside the block that checks download_response.headers and add "import re"
at the top of the file alongside the other imports in opensubtitles.py; ensure
the code that uses re.search (the block referencing download_response.headers
and file_name) continues to work unchanged after removing the local import.
In `@subtitle-downloader/templates/index.html`:
- Around line 45-67: The language select in index.html is hardcoded and
duplicates Config.LANGUAGES; update the template to populate options dynamically
instead of static <select> entries by either (a) passing Config.LANGUAGES from
app.py's index() route into render_template() and iterating over it in
index.html to emit <option>s (reference: Config.LANGUAGES and index() /
render_template), or (b) fetching /api/languages on page load and filling the
element with id="language" via JavaScript; remove the hardcoded <option> lines
and ensure the select keeps id="language" and name="language" and the empty
"Select Language" placeholder option remains.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a00d26da-9fa6-4ffb-a71d-8c38ab9226c5
📒 Files selected for processing (10)
subtitle-downloader/.gitignoresubtitle-downloader/app.pysubtitle-downloader/config.pysubtitle-downloader/requirements.txtsubtitle-downloader/static/css/style.csssubtitle-downloader/static/js/script.jssubtitle-downloader/subtitle_handler/__init__.pysubtitle-downloader/subtitle_handler/opensubtitles.pysubtitle-downloader/subtitle_handler/utils.pysubtitle-downloader/templates/index.html
| env = os.environ.get('FLASK_ENV', 'development') | ||
| app.config.from_object(config[env]) |
There was a problem hiding this comment.
Guard unsupported FLASK_ENV values at startup.
Lines 18-19 index the config registry directly, so a typo or unexpected env value will fail during import and the app never starts. Please switch to an explicit lookup and raise a clear startup error or apply a deliberate fallback.
Suggested fix
env = os.environ.get('FLASK_ENV', 'development')
-app.config.from_object(config[env])
+config_obj = config.get(env)
+if config_obj is None:
+ raise RuntimeError(f"Unsupported FLASK_ENV: {env!r}")
+app.config.from_object(config_obj)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| env = os.environ.get('FLASK_ENV', 'development') | |
| app.config.from_object(config[env]) | |
| env = os.environ.get('FLASK_ENV', 'development') | |
| config_obj = config.get(env) | |
| if config_obj is None: | |
| raise RuntimeError(f"Unsupported FLASK_ENV: {env!r}") | |
| app.config.from_object(config_obj) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/app.py` around lines 18 - 19, Current code indexes config
with config[env] which will raise a KeyError on typos/unexpected FLASK_ENV
values; change to an explicit lookup: check whether env is in the config
registry (e.g., if env not in config) and either raise a clear startup error
(RuntimeError with a message including the invalid env and allowed keys) or
assign a deliberate fallback (e.g., fallback = 'development') before calling
app.config.from_object; use the env variable name and
app.config.from_object(config_lookup) to locate the change points.
| try: | ||
| data = request.get_json() | ||
|
|
||
| if not data: | ||
| return jsonify({'error': 'No JSON data provided'}), 400 | ||
|
|
||
| series_name = data.get('series_name', '').strip() | ||
| season_episode = data.get('season_episode', '').strip() | ||
| language = data.get('language', '').strip() | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "app.py" -path "*/subtitle-downloader/*" -type fRepository: f/prompts.chat
Length of output: 86
🏁 Script executed:
cat -n ./subtitle-downloader/app.py | head -100Repository: f/prompts.chat
Length of output: 3501
🌐 Web query:
Flask request.get_json() behavior malformed JSON exception werkzeug
💡 Result:
flask.request.get_json() is implemented by Werkzeug’s Request.get_json(), and its error behavior depends on (a) whether the request claims to be JSON and (b) whether parsing succeeds.
What happens on malformed JSON?
- If the request has a JSON content type (e.g.
Content-Type: application/json) but the body is invalid JSON,get_json()callson_json_loading_failed(); by default this raises awerkzeug.exceptions.BadRequest(HTTP 400). [1]
What happens if Content-Type isn’t JSON?
- If the mimetype does not indicate JSON, Werkzeug will also call
on_json_loading_failed()(withe=None). In newer Werkzeug/Flask stacks this commonly surfaces as HTTP 415 Unsupported Media Type (this changed over time; see changelogs). [1][2][3]
Parameters that control this
silent=True: suppresses mimetype/parsing errors and returnsNoneinstead of raising. [1]force=True: ignores the mimetype and tries to parse JSON anyway. [1]cache=True: caches the parsed result for subsequent calls (and Flask fixed some edge cases around caching failures withsilent=True). [1][2]
Practical patterns
1) Return a clean 400 instead of an exception page
from flask import request
from werkzeug.exceptions import BadRequest
try:
data = request.get_json() # default: raises on bad JSON
except BadRequest:
return {"error": "Malformed JSON"}, 4002) Treat malformed JSON the same as “no JSON provided”
data = request.get_json(silent=True)
if data is None:
return {"error": "Missing or invalid JSON"}, 4003) Centralize handling
- Register an error handler for
BadRequest, or overrideRequest.on_json_loading_failed()to customize the response. The default behavior is to raiseBadRequest. [1]
Sources: Werkzeug Request.get_json / on_json_loading_failed docs (including 400 vs 415 notes) [1], Flask changelog notes about get_json behavior [2], Werkzeug changelog notes about 415 vs 400 behavior changes [3].
[1] (werkzeug.palletsprojects.com)
[2] (flask.palletsprojects.com)
[3] (werkzeug.palletsprojects.com)
Citations:
- 1: https://werkzeug.palletsprojects.com/en/stable/wrappers/?utm_source=openai
- 2: https://flask.palletsprojects.com/en/stable/changes/?utm_source=openai
- 3: https://werkzeug.palletsprojects.com/en/latest/changes/?utm_source=openai
🏁 Script executed:
python3 <<'EOF'
# Test dict.get() behavior with different value types
data = {'key_with_null': None, 'key_with_num': 42, 'key_missing': None}
print("=== dict.get() behavior ===")
print(f"Missing key with default '': {repr(data.get('key_missing', ''))}")
print(f"Null value with default '': {repr(data.get('key_with_null', ''))}")
print(f"Numeric value with default '': {repr(data.get('key_with_num', ''))}")
print("\n=== str.strip() behavior ===")
try:
result = None.strip()
except AttributeError as e:
print(f"None.strip() → AttributeError: {e}")
try:
result = 42.strip()
except AttributeError as e:
print(f"42.strip() → AttributeError: {e}")
print(f"' hello '.strip() → {repr(' hello '.strip())}")
EOFRepository: f/prompts.chat
Length of output: 161
🏁 Script executed:
python3 <<'EOF'
# Test dict.get() behavior with different value types
data = {'key_with_null': None, 'key_with_num': 42, 'key_missing': None}
print("=== dict.get() behavior ===")
print(f"Missing key with default '': {repr(data.get('key_missing', ''))}")
print(f"Null value with default '': {repr(data.get('key_with_null', ''))}")
print(f"Numeric value with default '': {repr(data.get('key_with_num', ''))}")
print("\n=== str.strip() behavior ===")
try:
None.strip()
except AttributeError as e:
print(f"None.strip() → AttributeError: {e}")
try:
num = 42
num.strip()
except AttributeError as e:
print(f"42.strip() → AttributeError: {e}")
print(f"' hello '.strip() → {repr(' hello '.strip())}")
EOFRepository: f/prompts.chat
Length of output: 387
🏁 Script executed:
cat -n ./subtitle-downloader/app.py | sed -n '43,82p'Repository: f/prompts.chat
Length of output: 1658
Bad JSON and type mismatches currently return 500 instead of 400.
Lines 44–51 can fail before proper validation. request.get_json() raises BadRequest on malformed JSON; dict.get('key', '') returns None (not the default) when the key exists with a null value or non-string; and .strip() on None or numeric types raises AttributeError. Because the exception handler at lines 80–82 catches all exceptions indiscriminately, these client-side errors are returned as 500 instead of 400. Use silent=True to suppress BadRequest, validate the data structure and field types before calling .strip(), and let only unexpected server errors reach the broad exception handler.
Suggested fix
- data = request.get_json()
+ data = request.get_json(silent=True)
- if not data:
- return jsonify({'error': 'No JSON data provided'}), 400
+ if not isinstance(data, dict):
+ return jsonify({'error': 'Invalid JSON body'}), 400
- series_name = data.get('series_name', '').strip()
- season_episode = data.get('season_episode', '').strip()
- language = data.get('language', '').strip()
+ series_name = data.get('series_name')
+ season_episode = data.get('season_episode')
+ language = data.get('language')
+
+ if not all(isinstance(value, str) for value in (series_name, season_episode, language)):
+ return jsonify({
+ 'error': 'series_name, season_episode, and language must be strings'
+ }), 400
+
+ series_name = series_name.strip()
+ season_episode = season_episode.strip()
+ language = language.strip()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/app.py` around lines 43 - 52, Change request.get_json()
to request.get_json(silent=True) and validate before calling .strip(): ensure
the returned data is a dict (return jsonify error 400 if not), then for each
field (series_name, season_episode, language) fetch raw = data.get('field') and
if raw is None or not isinstance(raw, str) return a 400 JSON error; only after
those checks call raw.strip() into series_name/season_episode/language. Also
adjust the broad exception handling so that client-side issues (malformed JSON,
type errors, missing fields) return 400 rather than being swallowed as 500—e.g.,
explicitly return 400 for known client errors and let unexpected exceptions
propagate to the existing server-error handler.
| app.run( | ||
| host='0.0.0.0', | ||
| port=5000, | ||
| debug=app.config['DEBUG'] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "app.py" | head -20Repository: f/prompts.chat
Length of output: 86
🏁 Script executed:
head -160 subtitle-downloader/app.py | tail -30Repository: f/prompts.chat
Length of output: 825
🏁 Script executed:
cat -n subtitle-downloader/app.py | sed -n '130,160p'Repository: f/prompts.chat
Length of output: 693
🏁 Script executed:
head -50 subtitle-downloader/app.pyRepository: f/prompts.chat
Length of output: 1270
🏁 Script executed:
grep -n "DEBUG\|config\|Config" subtitle-downloader/app.py | head -20Repository: f/prompts.chat
Length of output: 318
🏁 Script executed:
find . -type f -name "config.py" | head -10Repository: f/prompts.chat
Length of output: 89
🏁 Script executed:
cat -n subtitle-downloader/config.pyRepository: f/prompts.chat
Length of output: 1985
Default Flask dev server to 127.0.0.1 instead of 0.0.0.0.
Lines 145-148 bind the dev server to all network interfaces. While this code only runs via python app.py (not in production), unnecessarily exposing the dev server to the network increases risk, especially when debug mode is enabled. Default to 127.0.0.1 (localhost only) and allow wider binding through explicit environment variable configuration.
Suggested fix
app.run(
- host='0.0.0.0',
+ host=os.environ.get('HOST', '127.0.0.1'),
port=5000,
debug=app.config['DEBUG']
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| app.run( | |
| host='0.0.0.0', | |
| port=5000, | |
| debug=app.config['DEBUG'] | |
| app.run( | |
| host=os.environ.get('HOST', '127.0.0.1'), | |
| port=5000, | |
| debug=app.config['DEBUG'] | |
| ) |
🧰 Tools
🪛 Ruff (0.15.5)
[error] 146-146: Possible binding to all interfaces
(S104)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/app.py` around lines 145 - 148, Change the dev server
bind in the app.run call so it defaults to localhost instead of all interfaces:
replace the hardcoded '0.0.0.0' host in the app.run(...) invocation with a
configurable value that defaults to '127.0.0.1' (e.g., read from an environment
variable such as FLASK_RUN_HOST or a custom HOST env var) while keeping the
existing app.config['DEBUG'] usage; update the app.run call site so it uses that
env-derived host to allow explicit wider binding when needed.
|
|
||
| class Config: | ||
| """Base configuration""" | ||
| SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' |
There was a problem hiding this comment.
Hardcoded fallback secret key is a security risk in production.
The fallback 'dev-secret-key-change-in-production' will be used if the environment variable is not set, which could lead to predictable session tokens in production. Consider failing explicitly when SECRET_KEY is not set in production mode.
🛡️ Proposed fix
class Config:
"""Base configuration"""
- SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
+ SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
TESTING = FalseThen in ProductionConfig:
class ProductionConfig(Config):
"""Production configuration"""
DEBUG = False
+
+ def __init__(self):
+ if not self.SECRET_KEY:
+ raise ValueError("SECRET_KEY environment variable must be set in production")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' | |
| SECRET_KEY = os.environ.get('SECRET_KEY') |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/config.py` at line 5, The config currently sets
SECRET_KEY = os.environ.get('SECRET_KEY') or
'dev-secret-key-change-in-production', which provides a dangerous production
fallback; remove the hardcoded fallback and make ProductionConfig require the
env var explicitly (e.g. read via os.environ['SECRET_KEY'] or validate and raise
RuntimeError/AssertionError if SECRET_KEY is missing) so that the app fails to
start when SECRET_KEY is not provided; update the SECRET_KEY declaration and/or
override it in the ProductionConfig class (referencing SECRET_KEY and
ProductionConfig) to enforce this check and include a clear error message.
| @@ -0,0 +1,3 @@ | |||
| Flask==2.3.0 | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check latest Flask version and security advisories
curl -s https://pypi.org/pypi/flask/json | jq '.info.version'
# Check for security advisories
gh api graphql -f query='
{
securityVulnerabilities(first: 5, ecosystem: PIP, package: "flask") {
nodes {
advisory {
summary
severity
publishedAt
}
vulnerableVersionRange
firstPatchedVersion {
identifier
}
}
}
}'Repository: f/prompts.chat
Length of output: 226
🌐 Web query:
Flask 2.3.0 security vulnerabilities GHSA-m2qf-hxjv-5gpq GHSA-68rp-wp8r-4726
💡 Result:
Flask 2.3.0 is affected by both advisories
| Advisory | What it is | Affected versions (includes Flask 2.3.0) | Fix |
|---|---|---|---|
| GHSA-m2qf-hxjv-5gpq (CVE-2023-30861) | Missing Vary: Cookie in a session refresh edge-case can let a caching proxy cache a response (and potentially a Set-Cookie), risking session-cookie disclosure when specific conditions are met. |
NVD lists affected as 2.3.0 to < 2.3.2 (and older branches up to < 2.2.5). [2] GitHub advisory describes the cache/proxy + session.permanent=True conditions and patched releases. [1] |
Upgrade to 2.3.2 (or 2.2.5 on the 2.2 line). [1][2] |
| GHSA-68rp-wp8r-4726 (CVE-2026-27205) | Session access via some patterns (notably the Python in operator) may fail to add Vary: Cookie, enabling cache poisoning / sensitive-response caching behind certain proxies. |
3.1.2 and below (i.e., < 3.1.3). [3][4] | Upgrade to 3.1.3. [3][4] |
Practical remediation for a Flask 2.3.0 app
- For GHSA-m2qf-hxjv-5gpq: upgrade at least to 2.3.2. [1][2]
- For GHSA-68rp-wp8r-4726: there is no fix in the 2.3.x line in the advisory data; upgrading to 3.1.3 is the stated fix. [3][4]
- If you can’t upgrade immediately, mitigate cache risk by ensuring responses are not cached (e.g., set
Cache-Control: private, no-storewhere appropriate) and avoid relying on session access patterns that might not triggerVary: Cookie. [3][4]
Sources: GitHub Security Advisory for GHSA-m2qf-hxjv-5gpq [1], NVD CVE-2023-30861 [2], OSV GHSA-68rp-wp8r-4726 [3], NVD CVE-2026-27205 [4].
🌐 Web query:
Flask 2.3.0 session cookie vulnerability Vary Cookie header
💡 Result:
Flask 2.3.0 is affected by CVE-2023-30861: under certain conditions, Flask may send a refreshed permanent session cookie (Set-Cookie) without also adding Vary: Cookie, which can let a shared/caching proxy cache and replay a response (and potentially the Set-Cookie header) across different clients. This can result in disclosure of another user’s session cookie. [1][2]
When it can happen (all must apply): behind a caching proxy that doesn’t ignore/strip cookies, session.permanent = True, your view does not access/modify session, SESSION_REFRESH_EACH_REQUEST=True (default), and you don’t mark responses as private/not cacheable (e.g., missing protective Cache-Control). [1][2]
Fix: upgrade Flask to 2.3.2+ (or 2.2.5 on the 2.2.x line). [1][2]
Mitigations if you can’t upgrade immediately:
- Ensure sensitive/session-dependent responses are not shared-cached (e.g.,
Cache-Control: privateorno-store). [1][2] - (Defense-in-depth) Add
Vary: Cookieon responses where a session cookie might be refreshed. [1][2]
Sources: GitHub Advisory (GHSA-m2qf-hxjv-5gpq) / CVE-2023-30861, and NVD entry. [1][2]
🌐 Web query:
Flask security advisory PYSEC-2023-62
💡 Result:
PYSEC-2023-62 (Flask) maps to CVE-2023-30861 / GHSA-m2qf-hxjv-5gpq: a scenario where a caching proxy can cache and replay responses across clients, potentially causing one client’s permanent session cookie (Set-Cookie) to be sent to other clients when Vary: Cookie is missing/insufficient. [1]
When it can happen (all required):
- App is behind a caching proxy that might cache responses even with cookies. [1]
session.permanent = True. [1]- The request does not access/modify the session, but the session cookie gets refreshed. [1]
SESSION_REFRESH_EACH_REQUEST = True(default). [1]- No
Cache-Controlmarking the response as private / not cacheable. [1]
Affected versions (per PyPI advisory aggregations):
- Flask < 2.2.5, and 2.3.0–2.3.1 (i.e.,
>2.3.0, <2.3.2). [2]
Fixed versions:
- Upgrade to Flask 2.2.5 (2.2.x line) or Flask 2.3.2 (2.3.x line). [1][2]
Mitigations (if you can’t upgrade immediately):
- Ensure responses that may set/refresh cookies are not cacheable (e.g., appropriate
Cache-Control), and/or configure your proxy/CDN to not cache responses withSet-Cookie/ vary on cookies correctly. [1]
Sources: GitHub Security Advisory (Flask) [1]; PYSEC-2023-62 aggregation with affected ranges [2].
[1] (github.com)
[2] (resolvedsecurity.com)
Citations:
Upgrade Flask from 2.3.0 to patch critical session cookie vulnerabilities.
Flask 2.3.0 is affected by two HIGH severity vulnerabilities:
- GHSA-m2qf-hxjv-5gpq (CVE-2023-30861): Missing
Vary: Cookieheader in session refresh scenarios can allow caching proxies to leak session cookies across clients whensession.permanent=Trueand responses lack protectiveCache-Controlheaders. Fixed in Flask 2.3.2+ (or 2.2.5 for 2.2.x line). - GHSA-68rp-wp8r-4726 (CVE-2026-27205): Session access via certain patterns (e.g., Python
inoperator) may fail to addVary: Cookie, enabling cache poisoning behind shared proxies. Requires Flask 3.1.3+ (no patch available in 2.3.x line).
Minimum remediation: upgrade to Flask 2.3.2. For complete coverage of both vulnerabilities, upgrade to 3.1.3.
🧰 Tools
🪛 OSV Scanner (2.3.3)
[HIGH] 1-1: flask 2.3.0: undefined
(PYSEC-2023-62)
[HIGH] 1-1: flask 2.3.0: Flask session does not add Vary: Cookie header when accessed in some ways
[HIGH] 1-1: flask 2.3.0: Flask vulnerable to possible disclosure of permanent session cookie due to missing Vary: Cookie header
[HIGH] 1-1: flask 2.3.0: undefined
(PYSEC-2023-62)
[HIGH] 1-1: flask 2.3.0: Flask session does not add Vary: Cookie header when accessed in some ways
[HIGH] 1-1: flask 2.3.0: Flask vulnerable to possible disclosure of permanent session cookie due to missing Vary: Cookie header
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/requirements.txt` at line 1, Update the Flask dependency
entry in requirements.txt from "Flask==2.3.0" to a patched version; change the
line to "Flask==2.3.2" (minimum remediation) or "Flask==3.1.3" for full coverage
of both CVEs, then re-run any lock/installation steps (e.g., pip install -r
requirements.txt or your dependency lock generation) to ensure the updated
package is applied; locate the single dependency line "Flask==2.3.0" in
requirements.txt and replace it accordingly.
| font-weight: 600; | ||
| color: #333; | ||
| margin-bottom: 6px; | ||
| word-break: break-word; |
There was a problem hiding this comment.
Use overflow-wrap: break-word instead of deprecated word-break: break-word.
The keyword break-word for word-break is deprecated. Use overflow-wrap: break-word for the same effect with better browser support.
📝 Proposed fix
.subtitle-name {
font-weight: 600;
color: `#333`;
margin-bottom: 6px;
- word-break: break-word;
+ overflow-wrap: break-word;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| word-break: break-word; | |
| .subtitle-name { | |
| font-weight: 600; | |
| color: `#333`; | |
| margin-bottom: 6px; | |
| overflow-wrap: break-word; | |
| } |
🧰 Tools
🪛 Stylelint (17.4.0)
[error] 239-239: Unexpected deprecated keyword "break-word" for property "word-break" (declaration-property-value-keyword-no-deprecated)
(declaration-property-value-keyword-no-deprecated)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/static/css/style.css` at line 239, Replace the deprecated
CSS declaration using "word-break: break-word" by removing that property and
adding "overflow-wrap: break-word" (and optionally keep "word-break: normal" for
explicit fallback) wherever "word-break: break-word" appears in the stylesheet
so the rule is updated to use the modern, better-supported overflow-wrap
behavior.
| if (subtitle.download_count !== undefined) { | ||
| const downloadSpan = document.createElement('span'); | ||
| downloadSpan.className = 'meta-item'; | ||
| downloadSpan.textContent = `📥 ${subtitle.downloads || 0} downloads`; | ||
| metaDiv.appendChild(downloadSpan); | ||
| } |
There was a problem hiding this comment.
Field name mismatch: checking download_count but API returns downloads.
The condition checks subtitle.download_count but the API response from opensubtitles.py (line 73) uses the field name downloads. This condition will always be falsy, hiding download count metadata.
🐛 Proposed fix
- if (subtitle.download_count !== undefined) {
+ if (subtitle.downloads !== undefined) {
const downloadSpan = document.createElement('span');
downloadSpan.className = 'meta-item';
downloadSpan.textContent = `📥 ${subtitle.downloads || 0} downloads`;
metaDiv.appendChild(downloadSpan);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/static/js/script.js` around lines 156 - 161, The
condition incorrectly checks subtitle.download_count while the API returns
subtitle.downloads, so update the check in the block that creates downloadSpan
to use subtitle.downloads (e.g., change the condition from
subtitle.download_count !== undefined to subtitle.downloads !== undefined or use
a truthy check) and ensure the created downloadSpan still uses
subtitle.downloads (and falls back to 0) before appending it to metaDiv.
| if 'data' in data: | ||
| for lang in data['data']: | ||
| languages[lang.get('code')] = lang.get('name') | ||
|
|
||
| return languages | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error fetching languages: {str(e)}") | ||
| return Config.LANGUAGES |
There was a problem hiding this comment.
Fallback returns inconsistent data structure, breaking API contract.
On success, get_supported_languages() returns {code: name} (e.g., {'en': 'English'}), but the fallback on line 162 returns Config.LANGUAGES which has {name: code} structure (e.g., {'English': 'en'}). This breaks the /api/languages endpoint contract for clients.
🐛 Proposed fix to invert the fallback structure
except Exception as e:
logger.error(f"Error fetching languages: {str(e)}")
- return Config.LANGUAGES
+ # Invert Config.LANGUAGES to match API response structure {code: name}
+ return {code: name for name, code in Config.LANGUAGES.items()}🧰 Tools
🪛 Ruff (0.15.5)
[warning] 160-160: Do not catch blind exception: Exception
(BLE001)
[warning] 161-161: Use explicit conversion flag
Replace with conversion flag
(RUF010)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/subtitle_handler/opensubtitles.py` around lines 154 -
162, get_supported_languages currently returns a dict mapping language codes to
names (languages variable) but on exception returns Config.LANGUAGES which is
the inverse mapping (name->code); change the exception return to transform
Config.LANGUAGES into the same {code: name} shape before returning (e.g.,
iterate Config.LANGUAGES items and build {code: name}) so the API contract is
preserved; update the except block that logs via logger.error to return the
transformed mapping instead of Config.LANGUAGES directly.
| Returns: | ||
| dict: {'valid': bool, 'error': str or None} |
There was a problem hiding this comment.
Docstring does not match return type.
The docstring states the return is {'valid': bool, 'error': str or None} but the function actually returns {'valid': bool, 'errors': list} (line 44-47).
📝 Proposed fix
Returns:
- dict: {'valid': bool, 'error': str or None}
+ dict: {'valid': bool, 'errors': list}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Returns: | |
| dict: {'valid': bool, 'error': str or None} | |
| Returns: | |
| dict: {'valid': bool, 'errors': list} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@subtitle-downloader/subtitle_handler/utils.py` around lines 16 - 17, The
docstring for the function in subtitle_handler.utils that documents a return of
{'valid': bool, 'error': str or None} is out of sync with the implementation
which returns {'valid': bool, 'errors': list}; update the docstring to describe
the actual return shape (e.g., {'valid': bool, 'errors': list[str]} and what
items in the list mean) or alternatively change the function to return a single
'error' string to match the current docstring—locate the function by the return
statement that constructs {'valid': ..., 'errors': ...} and make the docstring
and implementation consistent.
Description
This PR introduces a complete web application for downloading TV show subtitles from OpenSubtitles API. The application includes:
Key Features
Files Added
app.py- Flask application with API endpointssubtitle_handler/opensubtitles.py- OpenSubtitles API clientsubtitle_handler/utils.py- Input validation and parsing utilitiessubtitle_handler/__init__.py- Module exportsconfig.py- Configuration management (dev/prod/test)templates/index.html- HTML template with form and results displaystatic/css/style.css- Complete styling with responsive designstatic/js/script.js- Frontend logic for search, download, and UI interactionsrequirements.txt- Python dependencies (Flask, requests, python-dotenv).gitignore- Standard Python project ignoresType of Change
Test Plan
The application can be tested by:
pip install -r requirements.txtpython app.pyhttp://localhost:5000https://claude.ai/code/session_01Xg36Q4fJoN1hKHsbpYbNCS
Summary by CodeRabbit