Skip to content

Feature/add playlist support#408

Open
karam-ajaj wants to merge 5 commits intoDialmasterOrg:devfrom
karam-ajaj:feature/add-playlist-support
Open

Feature/add playlist support#408
karam-ajaj wants to merge 5 commits intoDialmasterOrg:devfrom
karam-ajaj:feature/add-playlist-support

Conversation

@karam-ajaj
Copy link

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:

image image image

- 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
Copilot AI review requested due to automatic review settings January 29, 2026 15:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
}
}

/** * Get paginated playlist videos
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
});

// Create playlistvideos join table (similar to channelvideos)
await queryInterface.createTable('playlistvideos', {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +697 to +698
const regex = new RegExp(playlist.title_filter_regex, 'i');
filteredVideos = filteredVideos.filter(v => regex.test(v.title));
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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'
);
}

Copilot uses AI. Check for mistakes.
pageSize: pageSize.toString(),
hideDownloaded: hideDownloaded.toString(),
});
if (searchQuery) params.append('search', searchQuery);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'.

Suggested change
if (searchQuery) params.append('search', searchQuery);
if (searchQuery) params.append('searchQuery', searchQuery);

Copilot uses AI. Check for mistakes.
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';
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import IndeterminateCheckBoxIcon.

Suggested change
import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox';

Copilot uses AI. Check for mistakes.
const PlaylistVideo = require('../models/playlistvideo');
const Video = require('../models/video');
const MessageEmitter = require('./messageEmitter.js');
const { Op, fn, col, where } = require('sequelize');
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable where.

Suggested change
const { Op, fn, col, where } = require('sequelize');
const { Op, fn, col } = require('sequelize');

Copilot uses AI. Check for mistakes.
const Video = require('../models/video');
const MessageEmitter = require('./messageEmitter.js');
const { Op, fn, col, where } = require('sequelize');
const fileCheckModule = require('./fileCheckModule');
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable fileCheckModule.

Suggested change
const fileCheckModule = require('./fileCheckModule');

Copilot uses AI. Check for mistakes.
const { spawn } = require('child_process');

const SUB_FOLDER_DEFAULT_KEY = '__default__';
const MAX_LOAD_MORE_VIDEOS = 5000;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable MAX_LOAD_MORE_VIDEOS.

Suggested change
const MAX_LOAD_MORE_VIDEOS = 5000;

Copilot uses AI. Check for mistakes.
* @returns {express.Router}
*/
module.exports = function createPlaylistRoutes({ verifyToken, playlistModule }) {
const PlaylistVideo = require('../models/playlistvideo');
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable PlaylistVideo.

Suggested change
const PlaylistVideo = require('../models/playlistvideo');
require('../models/playlistvideo');

Copilot uses AI. Check for mistakes.
@dialmaster
Copy link
Collaborator

dialmaster commented Jan 29, 2026

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dialmaster self-requested a review January 31, 2026 17:59
Copy link
Collaborator

@dialmaster dialmaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. 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
  2. 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.
  3. 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.
  4. 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.
  5. The "Download all playlists" button doesn't create a proper Job record. Looking at downloadAllPlaylists() - it calls doSpecificDownloads() 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
  1. 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.
  2. 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.
  3. 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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use "alert()" for user notifications, please use proper patterns with Snackbar notifications like the rest of the FE uses.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All new modules require test coverage, this has none.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All new files require test coverage.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this file removed?

@dialmaster dialmaster requested a review from mkulina January 31, 2026 18:25
@mkulina
Copy link
Collaborator

mkulina commented Feb 1, 2026

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 Playlist and PlaylistVideo models aren't registered in server/models/index.js so they won't actually work. The frontend tries to show playlist.thumbnail but there's no thumbnail column in the database. Invalid regex patterns will crash the auto-download job (needs try/catch).

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.
There's also a fair amount of duplicated code between the channel and playlist implementations.

@hammamboumou
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants