Conversation
- Add backend playlist module with CRUD operations - Add database models for playlists and playlistvideos tables - Add migration for playlist tables creation - Add playlist API routes with full REST endpoints - Add PlaylistManager component with list view and operations - Add PlaylistPage with channel-page-like layout and features - Add PlaylistSettingsDialog matching ChannelSettingsDialog functionality - Add download tab for playlists in DownloadNew component - Add volume mount for migrations in docker-compose.yml - Implement 'Download New from All Playlists' functionality - Support playlist-level settings: quality, subfolder, duration filters, regex - Reuse SubfolderAutocomplete and other shared components - Match channel page UI/UX patterns for consistency
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive playlist support to Youtarr, allowing users to manage and download videos from YouTube playlists alongside existing channel functionality. The feature was primarily implemented using GitHub Copilot.
Changes:
- Added new database tables for playlists and playlist videos with associated models and migrations
- Implemented complete playlist management API with routes for CRUD operations, video fetching, filtering, and downloading
- Created React frontend components for playlist management, settings configuration, and video display
- Extended yt-dlp command builder with user-agent and player client arguments to improve YouTube compatibility
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| server/models/playlist.js | New model defining the playlist schema with settings like quality, filters, and auto-download |
| server/models/playlistvideo.js | New model for playlist video entries similar to channel videos |
| server/modules/playlistModule.js | Core business logic for playlist management, video fetching, filtering, and download scheduling |
| server/routes/playlists.js | Complete REST API endpoints for playlist operations |
| server/modules/download/ytdlpCommandBuilder.js | Added user-agent and iOS client arguments for YouTube compatibility |
| migrations/20260129000000-create-playlists-table.js | Database migration creating playlists and playlistvideos tables with indexes |
| client/src/types/Playlist.ts | TypeScript type definitions for playlist entities |
| client/src/components/PlaylistManager.tsx | Main playlist management interface with list view and CRUD operations |
| client/src/components/PlaylistPage.tsx | Detailed playlist view showing metadata and videos |
| client/src/components/PlaylistPage/PlaylistVideos.tsx | Video list component with grid/list views, search, and filtering |
| client/src/components/PlaylistPage/PlaylistSettingsDialog.tsx | Settings dialog for configuring playlist download parameters |
| client/src/components/PlaylistManager/hooks/*.ts | Custom React hooks for playlist data fetching and mutations |
| client/src/App.tsx | Added routing and navigation for playlist pages |
| client/src/components/DownloadManager/DownloadNew.tsx | Integrated playlist download tab into download manager |
| .husky/pre-commit | Deleted file that enforced linting, type checking, and tests before commits |
| downloads/README.md | Deleted README file (appears unintentional) |
Files not reviewed (1)
- client/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } | ||
|
|
||
| /** * Get a single playlist by ID |
There was a problem hiding this comment.
The JSDoc comment formatting is inconsistent. There's an extra space after the opening '/**' which doesn't match the pattern used in other JSDoc comments in this file (e.g., lines 98, 122, 148, 248).
| } | ||
| } | ||
|
|
||
| /** * Get paginated playlist videos |
There was a problem hiding this comment.
The JSDoc comment formatting is inconsistent. There's an extra space after the opening '/**' which doesn't match the pattern used in other JSDoc comments in this file.
| }); | ||
|
|
||
| // Create playlistvideos join table (similar to channelvideos) | ||
| await queryInterface.createTable('playlistvideos', { |
There was a problem hiding this comment.
The migration uses queryInterface.createTable directly instead of the createTableIfNotExists helper function that is consistently used throughout the codebase. This could cause issues if the migration is run multiple times. Consider using createTableIfNotExists from the helpers module for consistency and to prevent errors on re-runs.
| const regex = new RegExp(playlist.title_filter_regex, 'i'); | ||
| filteredVideos = filteredVideos.filter(v => regex.test(v.title)); |
There was a problem hiding this comment.
Invalid regex patterns from user input will cause the RegExp constructor to throw an error, which will be caught by the outer try-catch but could silently fail for that specific playlist during auto-download. Consider wrapping the regex creation and filtering in a try-catch block to log a warning about invalid regex and skip the filter instead of failing the entire playlist download.
| const regex = new RegExp(playlist.title_filter_regex, 'i'); | |
| filteredVideos = filteredVideos.filter(v => regex.test(v.title)); | |
| try { | |
| const regex = new RegExp(playlist.title_filter_regex, 'i'); | |
| filteredVideos = filteredVideos.filter(v => regex.test(v.title)); | |
| } catch (regexError) { | |
| logger.warn( | |
| { | |
| playlistId: playlist.playlist_id, | |
| titleFilterRegex: playlist.title_filter_regex, | |
| error: regexError.message | |
| }, | |
| 'Invalid title_filter_regex for playlist; skipping title filter during auto-download' | |
| ); | |
| } |
| pageSize: pageSize.toString(), | ||
| hideDownloaded: hideDownloaded.toString(), | ||
| }); | ||
| if (searchQuery) params.append('search', searchQuery); |
There was a problem hiding this comment.
The query parameter name mismatch will prevent search functionality from working. The client sends 'search' as the query parameter name (line 84 in PlaylistVideos.tsx), but the server expects 'searchQuery' (line 337 in server/routes/playlists.js). Either change the client to send 'searchQuery' or change the server to read from 'search'.
| if (searchQuery) params.append('search', searchQuery); | |
| if (searchQuery) params.append('searchQuery', searchQuery); |
| import BlockIcon from '@mui/icons-material/Block'; | ||
| import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; | ||
| import CheckBoxIcon from '@mui/icons-material/CheckBox'; | ||
| import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox'; |
There was a problem hiding this comment.
Unused import IndeterminateCheckBoxIcon.
| import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox'; |
| const PlaylistVideo = require('../models/playlistvideo'); | ||
| const Video = require('../models/video'); | ||
| const MessageEmitter = require('./messageEmitter.js'); | ||
| const { Op, fn, col, where } = require('sequelize'); |
There was a problem hiding this comment.
Unused variable where.
| const { Op, fn, col, where } = require('sequelize'); | |
| const { Op, fn, col } = require('sequelize'); |
| const Video = require('../models/video'); | ||
| const MessageEmitter = require('./messageEmitter.js'); | ||
| const { Op, fn, col, where } = require('sequelize'); | ||
| const fileCheckModule = require('./fileCheckModule'); |
There was a problem hiding this comment.
Unused variable fileCheckModule.
| const fileCheckModule = require('./fileCheckModule'); |
| const { spawn } = require('child_process'); | ||
|
|
||
| const SUB_FOLDER_DEFAULT_KEY = '__default__'; | ||
| const MAX_LOAD_MORE_VIDEOS = 5000; |
There was a problem hiding this comment.
Unused variable MAX_LOAD_MORE_VIDEOS.
| const MAX_LOAD_MORE_VIDEOS = 5000; |
| * @returns {express.Router} | ||
| */ | ||
| module.exports = function createPlaylistRoutes({ verifyToken, playlistModule }) { | ||
| const PlaylistVideo = require('../models/playlistvideo'); |
There was a problem hiding this comment.
Unused variable PlaylistVideo.
| const PlaylistVideo = require('../models/playlistvideo'); | |
| require('../models/playlistvideo'); |
|
This is just my initial pass review, but there are some serious issues that need to be addressed before I can review in more depth. |
There was a problem hiding this comment.
You can't just delete the pre-commit hooks, this would have caught all the linting and test failures, that's why it's there.
dialmaster
left a comment
There was a problem hiding this comment.
I have a lot of concerns about this PR, especially the removal of the husky pre-commit hooks, the large number of linting and test failures, and the lack of test coverage for the new code.
The interaction between playlists and channels does not seem well thought out/planned.
- Videos are downloaded with the playlist's subfolder, not the channel's subfolder. If you have a playlist with videos from 10 different channels, they'll all go to one flat folder structure based on playlist settings, not organized by channel, even if the videos belong to channels that have configured settings
- There is no association between PlaylistVideo and Channel/Video models. If a video exists in both a channel AND a playlist, they're tracked separately. After downloading via playlist, the video won't appear in the channel's "downloaded" list.
- The auto-download functionality does not follow the channel grouping pattern... Channels use channelDownloadGrouper.generateDownloadGroups() to batch downloads by quality/subfolder for efficiency. Each playlist triggers a separate download job. If you have 20 playlists, that's 20 sequential jobs instead of batched downloads grouped by quality/subfolder.
- The channel system has channelDownloadFrequency in config. The playlist module references
const cronSchedule = config.playlistDownloadFrequency || '0 */6 * * *';but there's no UI to configure playlistDownloadFrequency - it's hardcoded to default. The user can't control when playlists auto-download separately from channels. - The "Download all playlists" button doesn't create a proper Job record. Looking at
downloadAllPlaylists()- it callsdoSpecificDownloads()for each playlist but doesn't create a parent "Playlist Downloads" job like channels have. This means:
- No way to see "Playlist Downloads" in the job history
- No protection against concurrent playlist download runs (unlike channels which check for existing jobs)
- Progress tracking is fragmented across multiple jobs
- The Playlist model has no thumbnail field, but the frontend PlaylistPage.tsx tries to display playlist.thumbnail. The migration doesn't include a thumbnail column for playlists. This will always be undefined.
- Channels integrate with archiveModule to track downloaded videos in yt-dlp's archive format. Playlists don't - meaning yt-dlp might re-download videos that were already downloaded.
- After channel downloads, plexModule.scanLibrary() is called. The playlist download flow doesn't trigger a Plex scan.
Overall... this feels like a first draft, that needs more careful thought to make sure this integrates into the Youtarr codebase properly. There is also a lot of duplicated or nearly duplicated code between the existing channel code and the new playlist code.
I am happy to discuss this with you, but something this large probably needs to start with a solid technical design plan.
| }; | ||
|
|
||
| const handleOpenPlaylistSettings = () => { | ||
| console.log('Opening playlist settings dialog'); |
There was a problem hiding this comment.
Please do not leave debug console.log() statements in your code.
| }; | ||
|
|
||
| const handleTriggerPlaylistDownloads = async (settings: DownloadSettings | null) => { | ||
| console.log('Triggering playlist downloads with settings:', settings); |
There was a problem hiding this comment.
Remove this and the rest of the "debug" console.log()s
| // If the result is a 400 then we already have a running Playlist Download | ||
| // job and we should display an alert | ||
| if (result.status === 400) { | ||
| alert('Playlist Download already running'); |
There was a problem hiding this comment.
Do not use "alert()" for user notifications, please use proper patterns with Snackbar notifications like the rest of the FE uses.
There was a problem hiding this comment.
All new modules require test coverage, this has none.
There was a problem hiding this comment.
All new files require test coverage.
There was a problem hiding this comment.
Why was this file removed?
|
Hey @karam-ajaj, appreciate you working on this. Playlist support has been requested a few times. I've reviewed your changes along side with @dialmaster. Build issues: The husky pre-commit hooks got removed, there are linting/test failures, and no tests for the new code. Bugs that will break things: The bigger problem is that playlists and channels don't really talk to each other. Videos go to the playlist's folder regardless of channel settings. If a playlist has videos from 10 channels, they all get dumped together instead of organized by channel. A video downloaded via playlist won't show as "downloaded" on the channel page since they're tracked completely separately. Channels batch downloads efficiently by quality/subfolder, but playlists just fire off separate jobs for each one. No archive integration, so yt-dlp might re-download stuff. No Plex scan after downloads either. There's a playlistDownloadFrequency config option referenced but no way to actually set it in the UI. |
|
This seems like an issue with a junior using copilot with no check. Just to know, is there a discussion open about playlist and channel integration? |
This was mostly done by GitHub copilot
Videos can be downloaded based on Playlists, it is tested and works fine and hopefully it can be merged
A database migration is needed because new columns are introduced
I published a new docker image to my dockerhub repo for testing https://hub.docker.com/r/keinstien/youtarr ,feel free to test it
Here are some screenshots: