Introduces end-to-end subtitles and quality selection across model, APIs, admin UI, and player.
Model: add video_file_low, subtitles[] plus legacy subtitle fields maintained for back-compat
API:
New GET/HEAD /api/subtitles/:id(/:subtitleId) to stream VTT with JWT/qs/cookie auth
Movies: POST /api/movies/:id/subtitles (upload .srt/.vtt, auto-convert SRT→VTT), DELETE /api/movies/:id/subtitles/:subtitleId; list/get now include subtitles; add/patch accept video_file_low and subtitle metadata; cache cleared on updates
Video: GET /api/video/:id supports ?quality=low to serve video_file_low
Player: adds quality toggle UI (high/low) with persistence; probes/uses ?quality=low; injects <track> from /api/subtitles/... with tokenized URLs; applies tracks on source switch
Admin UI: fields for video_file_low; subtitle upload, listing, removal; badges show "Low file" and "Subtitles"; add form wires initial subtitle upload
Python video server: supports ?quality=low selection of video_file_low
Misc: mount /api/subtitles; add subtitles/* to .gitignore; README disclaimer and Python README SSL run example
Written by Cursor Bugbot for commit 406a4f0bb3. This will update automatically on new commits. Configure here.
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Introduces end-to-end subtitles and quality selection across model, APIs, admin UI, and player.
>
> - Model: add `video_file_low`, `subtitles[]` plus legacy subtitle fields maintained for back-compat
> - API:
> - New `GET/HEAD /api/subtitles/:id(/:subtitleId)` to stream VTT with JWT/qs/cookie auth
> - Movies: `POST /api/movies/:id/subtitles` (upload .srt/.vtt, auto-convert SRT→VTT), `DELETE /api/movies/:id/subtitles/:subtitleId`; list/get now include `subtitles`; add/patch accept `video_file_low` and subtitle metadata; cache cleared on updates
> - Video: `GET /api/video/:id` supports `?quality=low` to serve `video_file_low`
> - Player: adds quality toggle UI (high/low) with persistence; probes/uses `?quality=low`; injects `<track>` from `/api/subtitles/...` with tokenized URLs; applies tracks on source switch
> - Admin UI: fields for `video_file_low`; subtitle upload, listing, removal; badges show "Low file" and "Subtitles"; add form wires initial subtitle upload
> - Python video server: supports `?quality=low` selection of `video_file_low`
> - Misc: mount `/api/subtitles`; add `subtitles/*` to `.gitignore`; README disclaimer and Python README SSL run example
>
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 406a4f0bb3a17e0e701f6d8bba1cb2eafe21f41e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
cursor[bot]
(Migrated from github.com)
left a comment
Copy Link
Copy Source
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
This PR is being reviewed by Cursor Bugbot
Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
<sup>Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the [Cursor dashboard](https://www.cursor.com/dashboard?tab=bugbot).</sup>
### This PR is being reviewed by Cursor Bugbot
<details>
<summary>Details</summary>
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the [Cursor dashboard](https://www.cursor.com/dashboard?tab=bugbot) to activate Pro and start your 14-day free trial.
</details>
The parseSubtitleDefault function (lines 78-86) and parseBooleanFlag function (lines 88-96) are completely identical implementations. Both parse boolean values from string inputs using the exact same logic (handling "true", "1", "yes", "on" for true, etc.). One function should be removed and the other reused in both places where they're currently called.
### Duplicate identical functions for boolean parsing
**Low Severity**
<!-- DESCRIPTION START -->
The `parseSubtitleDefault` function (lines 78-86) and `parseBooleanFlag` function (lines 88-96) are completely identical implementations. Both parse boolean values from string inputs using the exact same logic (handling `"true"`, `"1"`, `"yes"`, `"on"` for true, etc.). One function should be removed and the other reused in both places where they're currently called.
<!-- DESCRIPTION END -->
<!-- BUGBOT_BUG_ID: e8232935-88e0-46aa-a11d-f87144aa4467 -->
<!-- LOCATIONS START
routes/movies.js#L77-L96
LOCATIONS END -->
<a href="https://cursor.com/open?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjZkNTQ2MzE1LTRlMjEtNDc2OS05NTQ1LTc5YTNlMWM3MTY4NiIsImVuY3J5cHRpb25LZXkiOiJScDBtWV83TVA0cGhHaExqTk1qcTMxMFRMNjRkT1ptTXA1STlJbzJhYTU0IiwiYnJhbmNoIjoiMjAyNiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.Tj54I4d2hItocuhS5JBzM2-O2aW-J410ubBLfiKqPZr-0bxe47_Bio6DKJWXXjSPSHRgrUNInPX1YpqOspKmGX1vfLjblvVJZnQsMP1wr8pOW6r-nFnb9yWbOyHFdQDC9xxYqbbL_mW78DA38rof9b7dzleH0sRPWVtuS7zL7_Lx6-yLpa7aW9EMcm8w9L_-tnNVzCCZp2-dUpBmmvliS52uNPkvY-w7UoUkTb95WbKSTNCcTuHi9pcrOH1DdIIdr4yVaqdFqOwlndFp2GeWzvOrGbz5EqFjxL3E_OOUt0gxQPYHRIug8KSoofBYehEkrm3lSTOeIfUyLBX3eFp_Mw" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-cursor-light.svg"><img alt="Fix in Cursor" src="https://cursor.com/fix-in-cursor.svg"></picture></a> <a href="https://cursor.com/agents?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjZkNTQ2MzE1LTRlMjEtNDc2OS05NTQ1LTc5YTNlMWM3MTY4NiIsImVuY3J5cHRpb25LZXkiOiJScDBtWV83TVA0cGhHaExqTk1qcTMxMFRMNjRkT1ptTXA1STlJbzJhYTU0IiwiYnJhbmNoIjoiMjAyNiIsInJlcG9Pd25lciI6InF1b2lqZSIsInJlcG9OYW1lIjoib3NjYXJzLXBvb2wiLCJwck51bWJlciI6NzMsImNvbW1pdFNoYSI6IjQwNmE0ZjBiYjNhMTdlMGU3MDFmNmQ4YmJhMWNiMmVhZmUyMWY0MWUiLCJwcm92aWRlciI6ImdpdGh1YiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.bwaj7A2IDeyIJhlMwBvMDTlSe6utkA_s6CP2TFd-oI260RkzQ4swy6QFu0PN-XWzyUE2JiLOtbvH92C3hdxuzxgDkNUSPeu0qvLASkd4gzY4w0GKxVm7Ss0_b_UiCf52SOMOWoqImNunYo8hhdfETCGTaq-S9jmNvLHRWKjX8AAmRAct8p6aI1wC0Hc8zituGKge5fCBr2lLs46wKIu6-Xpfsei5lXsqL4d3MG0E5_Aaquogm760B-TWsOIy8lE5raelJmto4HULL5Cu0AxrmML_Gkq4KIZWI2QHpYWr9V5KShkUEE5jHMI8ijb5C0Qhi37WWsOP8Ot4czcHrUWm3A" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-web-light.svg"><img alt="Fix in Web" src="https://cursor.com/fix-in-web.svg"></picture></a>
When SRT to VTT conversion fails in the subtitle upload handler, the uploaded SRT file at req.file.path is not cleaned up. The convertSrtToVtt function only deletes the original file after successful conversion, so if readFile or writeFile throws (e.g., invalid encoding, disk full), the catch block returns a 400 error without deleting the orphaned file. Over time, failed conversions could accumulate orphaned files on disk.
### Failed SRT conversion leaks uploaded file on disk
**Low Severity**
<!-- DESCRIPTION START -->
When SRT to VTT conversion fails in the subtitle upload handler, the uploaded SRT file at `req.file.path` is not cleaned up. The `convertSrtToVtt` function only deletes the original file after successful conversion, so if `readFile` or `writeFile` throws (e.g., invalid encoding, disk full), the catch block returns a 400 error without deleting the orphaned file. Over time, failed conversions could accumulate orphaned files on disk.
<!-- DESCRIPTION END -->
<!-- BUGBOT_BUG_ID: ref1_2113dbc0-0525-4bb5-9dbb-32444c80baf3 -->
<!-- LOCATIONS START
routes/movies.js#L618-L624
LOCATIONS END -->
<a href="https://cursor.com/open?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmY4YTAwNzZmLWVhYzktNDUyZS1iNzMyLWI0YTlhYzE5N2VjMiIsImVuY3J5cHRpb25LZXkiOiJna3loaVE5SU9mZmhDRFYzQjRIQm50RjYzU0NyZ19LXzFtZXRreWMzWkljIiwiYnJhbmNoIjoiMjAyNiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.dVuuQKP7HyoShA2P652TOm66ol0N49HVrHOwdOCfJ_k__vfL3tuPHPjAo2jDWffTJmjA-Nf3QTSyyTFxc77clewavPsQGym2yRLze6pO_md6FKGuj9l5FlXPN8WU57KEOfQpDHMUnTyzQzpTHK3YObwxBPIgXlvifCVsIm2jzYKR18obAAeeCT6tfhEjzMhlkZ4a_2GhuhSFdc-XLQh23q5jVESNLu0tSMfv4iDqDbi6K90oWncB_D82IantWAkFtyRdBPBWylKVS87SfF9mdrVHcZMn2oBJGXMATq0GkSx2E_wyhcvt4AE2L1PMsr5xUndXdV8e_f0tYHLYyIrPww" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-cursor-light.svg"><img alt="Fix in Cursor" src="https://cursor.com/fix-in-cursor.svg"></picture></a> <a href="https://cursor.com/agents?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmY4YTAwNzZmLWVhYzktNDUyZS1iNzMyLWI0YTlhYzE5N2VjMiIsImVuY3J5cHRpb25LZXkiOiJna3loaVE5SU9mZmhDRFYzQjRIQm50RjYzU0NyZ19LXzFtZXRreWMzWkljIiwiYnJhbmNoIjoiMjAyNiIsInJlcG9Pd25lciI6InF1b2lqZSIsInJlcG9OYW1lIjoib3NjYXJzLXBvb2wiLCJwck51bWJlciI6NzMsImNvbW1pdFNoYSI6IjQwNmE0ZjBiYjNhMTdlMGU3MDFmNmQ4YmJhMWNiMmVhZmUyMWY0MWUiLCJwcm92aWRlciI6ImdpdGh1YiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.mouSMrGKiGp0p7nS8MDK9Srd6A9Uhpd1CiXYx9GKtgEZSwriBBKYOrxICTjhRRmUL0Nx_IYiX3bxxU3k2qOMarGaMfNdrfFDU8oPQ2QtKeaODi-YfuCE-hNdMvbs2kAvYnaovcJmVfz7rTzBDgVXTdpDq2sAUuIiidAE0GCsjSq0FS_lTi5r5DOluFPTMA7XiLLyGALV2wa8PO5hzE4xZkzyTdS7jtTkMvq6qKmUP5EGnTPLfhpCYI27QZ9yUj6fLfdiSeJ-pdTGYt1XHcy7qSIxadgyZGlY8AubOrOaZJdt0_jO6rPE-2MQaqy-qm-PBtwbjmpb8cVpoT5bjgMYMA" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-web-light.svg"><img alt="Fix in Web" src="https://cursor.com/fix-in-web.svg"></picture></a>
Movie deletion orphans associated subtitle files on disk
Medium Severity
When movies are deleted via the /delete endpoint, the associated subtitle files stored on disk in SUBTITLE_FILES_DIR are not removed. The endpoint queries movies with only select("imdb_id") and then calls Movie.deleteMany() without first retrieving or cleaning up the subtitles array file paths. This commit introduces subtitle file uploads but doesn't integrate cleanup into the movie deletion flow, causing subtitle files to accumulate indefinitely as movies are deleted.
### Movie deletion orphans associated subtitle files on disk
**Medium Severity**
<!-- DESCRIPTION START -->
When movies are deleted via the `/delete` endpoint, the associated subtitle files stored on disk in `SUBTITLE_FILES_DIR` are not removed. The endpoint queries movies with only `select("imdb_id")` and then calls `Movie.deleteMany()` without first retrieving or cleaning up the `subtitles` array file paths. This commit introduces subtitle file uploads but doesn't integrate cleanup into the movie deletion flow, causing subtitle files to accumulate indefinitely as movies are deleted.
<!-- DESCRIPTION END -->
<!-- BUGBOT_BUG_ID: ref2_681f0297-bcc5-47ca-8f39-f6e838d45b73 -->
<!-- LOCATIONS START
routes/movies.js#L755-L759
LOCATIONS END -->
<a href="https://cursor.com/open?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjYzZjllZDNjLWJlYTUtNGFmMy05MmNhLTUyMDA0ZDE2YzhiNCIsImVuY3J5cHRpb25LZXkiOiJhekRoY3hhbTlCb3JXX1V4bzBqSHQ4ZHprbjkzWUJCWmdpb2NjRnVYSDdzIiwiYnJhbmNoIjoiMjAyNiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.yW3cruYedER9TgoOPSuZZpW9Tkgr6qSh2cT7D3aJJb-LmdgjY5JzVDyftKaUv7jMm7j-wqP4MaggnRHc0YhXF6PivW-13_xl4QHKM1UP0klzHEDALTB9O-kFpGAv_7Q_IwUJbIf0uCEDRGi5AHBl2pAs2BcN34tyXfN0HIcHcHW8bjJSBR0-0bRpMsm03gwE32fabDFLwckolUkdoYu3AQbJvo7slMkXg491HCtSD0weGW_LEbH1AE0bIlavxRPHDifa17pTCgESWRBoH4GXPkUh-z-O4UAoNr3SqlmLWTC1_rJgP8hEUTiqmpJaU6Uq-jtc3TUe_tMe98JD6LLvXA" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-cursor-light.svg"><img alt="Fix in Cursor" src="https://cursor.com/fix-in-cursor.svg"></picture></a> <a href="https://cursor.com/agents?data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJ1Z2JvdC12MiJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjYzZjllZDNjLWJlYTUtNGFmMy05MmNhLTUyMDA0ZDE2YzhiNCIsImVuY3J5cHRpb25LZXkiOiJhekRoY3hhbTlCb3JXX1V4bzBqSHQ4ZHprbjkzWUJCWmdpb2NjRnVYSDdzIiwiYnJhbmNoIjoiMjAyNiIsInJlcG9Pd25lciI6InF1b2lqZSIsInJlcG9OYW1lIjoib3NjYXJzLXBvb2wiLCJwck51bWJlciI6NzMsImNvbW1pdFNoYSI6IjQwNmE0ZjBiYjNhMTdlMGU3MDFmNmQ4YmJhMWNiMmVhZmUyMWY0MWUiLCJwcm92aWRlciI6ImdpdGh1YiJ9LCJpYXQiOjE3NjkzMjUwMDMsImV4cCI6MTc3MTkxNzAwM30.hgznBaQfqEPS1bdrhXuRt8-uL6I4tpy0vMAamt5pz94fQxL28PVgh2k3OQ6uwNmpOX5NrsqLZz7CjhfhZbOgOrFTCrED6PeUCHq-kt2upr7kus-cgHpj4_dleKgwZe3KxQkE-5geUu5nsN_0fqb-34kZZEgYf7w16q2QRfQX6Cq8X2YJWZbZKMcAh8BtOWPpSd_uAesWAoz-7XLR1KBEHS3sSs5A0ZuYzvXBcVY4x3ze_sFf5BwQaGWj-fEl4z8bXRddHMlcgJx63OUfbfxQxhFYRxAWZUg4kRUTsz-LQGB1thLsaguMVLIARr3a3f3TVtsKhB8-A2mgSKaUnOEbpg" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/fix-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/fix-in-web-light.svg"><img alt="Fix in Web" src="https://cursor.com/fix-in-web.svg"></picture></a>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
This PR is being reviewed by Cursor Bugbot
Details
You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
@@ -65,0 +110,4 @@const file = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : "";if (!file) return null;return {_id: "legacy",Duplicate identical functions for boolean parsing
Low Severity
The
parseSubtitleDefaultfunction (lines 78-86) andparseBooleanFlagfunction (lines 88-96) are completely identical implementations. Both parse boolean values from string inputs using the exact same logic (handling"true","1","yes","on"for true, etc.). One function should be removed and the other reused in both places where they're currently called.Failed SRT conversion leaks uploaded file on disk
Low Severity
When SRT to VTT conversion fails in the subtitle upload handler, the uploaded SRT file at
req.file.pathis not cleaned up. TheconvertSrtToVttfunction only deletes the original file after successful conversion, so ifreadFileorwriteFilethrows (e.g., invalid encoding, disk full), the catch block returns a 400 error without deleting the orphaned file. Over time, failed conversions could accumulate orphaned files on disk.Movie deletion orphans associated subtitle files on disk
Medium Severity
When movies are deleted via the
/deleteendpoint, the associated subtitle files stored on disk inSUBTITLE_FILES_DIRare not removed. The endpoint queries movies with onlyselect("imdb_id")and then callsMovie.deleteMany()without first retrieving or cleaning up thesubtitlesarray file paths. This commit introduces subtitle file uploads but doesn't integrate cleanup into the movie deletion flow, causing subtitle files to accumulate indefinitely as movies are deleted.