Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions client/src/components/ChannelPage/ChannelVideos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import useMediaQuery from '../../hooks/useMediaQuery';
import { DownloadSettings } from '../DownloadManager/ManualDownload/types';
import { useVideoDeletion } from '../shared/useVideoDeletion';
import { useVideoProtection } from '../shared/useVideoProtection';
import { getVideoStatus } from '../../utils/videoStatus';
import { getVideoStatus, isRemovedStatus } from '../../utils/videoStatus';
import VideoCard from './VideoCard';
import VideoListItem from './VideoListItem';
import VideoTableView from './VideoTableView';
Expand Down Expand Up @@ -135,6 +135,7 @@ function ChannelVideos({
const [maxRating, setMaxRating] = useState('');
const [protectedFilter, setProtectedFilter] = useState<ChipFilterMode>('off');
const [missingFilter, setMissingFilter] = useState<ChipFilterMode>('off');
const [deletedFilter, setDeletedFilter] = useState<ChipFilterMode>('off');
const [ignoredFilter, setIgnoredFilter] = useState<ChipFilterMode>('off');
const [watchedFilter, setWatchedFilter] = useState<ChipFilterMode>('off');

Expand Down Expand Up @@ -291,6 +292,7 @@ function ChannelVideos({
filters.dateTo ? filters.dateTo.toISOString() : '',
protectedFilter,
missingFilter,
deletedFilter,
ignoredFilter,
watchedFilter,
useInfiniteScroll,
Expand All @@ -309,6 +311,7 @@ function ChannelVideos({
filters.dateTo,
protectedFilter,
missingFilter,
deletedFilter,
ignoredFilter,
watchedFilter,
useInfiniteScroll,
Expand Down Expand Up @@ -342,6 +345,7 @@ function ChannelVideos({
resetKey,
protectedFilter,
missingFilter,
deletedFilter,
ignoredFilter,
watchedFilter,
onFirstLoad: onVideosLoaded,
Expand Down Expand Up @@ -397,6 +401,7 @@ function ChannelVideos({
maxRating,
protectedFilter,
missingFilter,
deletedFilter,
ignoredFilter,
watchedFilter,
downloadedFilter,
Expand Down Expand Up @@ -514,6 +519,7 @@ function ChannelVideos({
maxRating,
protectedFilter,
missingFilter,
deletedFilter,
ignoredFilter,
watchedFilter,
downloadedFilter,
Expand All @@ -534,7 +540,7 @@ function ChannelVideos({
const downloadable = paginatedVideos
.filter((video) => {
const status = getVideoStatus(video);
return status === 'never_downloaded' || status === 'missing' || status === 'ignored';
return status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored';
})
.map((video) => video.youtube_id);
setCheckedBoxes((prev) => {
Expand All @@ -557,7 +563,7 @@ function ChannelVideos({
const ids = paginatedVideos
.filter((video) => {
const status = getVideoStatus(video);
return status === 'never_downloaded' || status === 'missing' || status === 'ignored';
return status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored';
})
.map((video) => video.youtube_id);
setCheckedBoxes((prev) => {
Expand Down Expand Up @@ -872,6 +878,7 @@ function ChannelVideos({
{ id: 'maxRating', value: maxRating, onChange: setMaxRating },
{ id: 'protected', value: protectedFilter, onChange: setProtectedFilter },
{ id: 'missing', value: missingFilter, onChange: setMissingFilter },
{ id: 'deleted', value: deletedFilter, onChange: setDeletedFilter },
{ id: 'ignored', value: ignoredFilter, onChange: setIgnoredFilter },
{ id: 'downloaded', value: downloadedFilter, onChange: setDownloadedFilter },
{ id: 'watched', value: watchedFilter, onChange: setWatchedFilter },
Expand All @@ -891,6 +898,7 @@ function ChannelVideos({
maxRating,
protectedFilter,
missingFilter,
deletedFilter,
ignoredFilter,
watchedFilter,
downloadedFilter,
Expand Down Expand Up @@ -1070,7 +1078,7 @@ function ChannelVideos({
// The summary already gives the tab's size; the list count only adds
// something while a search or filter narrows the list.
const listIsFiltered = Boolean(listState.search) || hasDurationOrDateFilter || maxRating !== ''
|| [downloadedFilter, protectedFilter, missingFilter, ignoredFilter, watchedFilter].some((mode) => mode !== 'off');
|| [downloadedFilter, protectedFilter, missingFilter, deletedFilter, ignoredFilter, watchedFilter].some((mode) => mode !== 'off');
let countChipLabel: string | null = null;
if (totalCount > 0 && listIsFiltered) {
countChipLabel = `${totalCount} ${totalCount === 1 ? 'match' : 'matches'}`;
Expand Down Expand Up @@ -1252,7 +1260,7 @@ function ChannelVideos({
disabled={
paginatedVideos.filter((video) => {
const status = getVideoStatus(video);
return status === 'never_downloaded' || status === 'missing' || status === 'ignored';
return status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored';
}).length === 0
}
className="intent-warning"
Expand Down
33 changes: 18 additions & 15 deletions client/src/components/ChannelPage/VideoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { CalendarToday as CalendarTodayIcon, Block as BlockIcon, CheckCircleOutl
import { formatDuration } from '../../utils';
import { ChannelVideo } from '../../types/ChannelVideo';
import { decodeHtml, formatAddedDateTime } from '../../utils/formatters';
import { getVideoStatus, getStatusColor, getStatusIcon, getStatusLabel, getMediaTypeInfo, getStatusChipVariant, getStatusChipStyle } from '../../utils/videoStatus';
import { getVideoStatus, getStatusColor, getStatusIcon, getStatusLabel, getMediaTypeInfo, getStatusChipVariant, getStatusChipStyle, isRemovedStatus } from '../../utils/videoStatus';
import { describeRow } from '../../utils/videoRemoval';
import StillLiveDot from './StillLiveDot';
import RatingBadge from '../shared/RatingBadge';
import DownloadFormatIndicator from '../shared/DownloadFormatIndicator';
Expand Down Expand Up @@ -58,7 +59,7 @@ function VideoCard({
const status = getVideoStatus(video);
// Check if video is still live (not "was_live" and not null/undefined)
const isStillLive = video.live_status && video.live_status !== 'was_live';
const isDownloadSelectable = (status === 'never_downloaded' || status === 'missing' || status === 'ignored') && !isStillLive;
const isDownloadSelectable = (status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored') && !isStillLive;
const isDeleteSelectable = video.added && !video.removed && !isStillLive;
const isDownloadAllowed = selectionMode !== 'delete';
const isDeleteAllowed = selectionMode !== 'download';
Expand Down Expand Up @@ -390,19 +391,21 @@ function VideoCard({
style={{ ...SHARED_STATUS_CHIP_SMALL_STYLE, flexShrink: 0 }}
/>
<WatchedChip watchedBy={video.watchedBy || []} />
<Chip
icon={getStatusIcon(status)}
label={statusLabel}
size="small"
color={getStatusColor(status)}
variant={getStatusChipVariant(status)}
style={{
flex: '0 0 auto',
minWidth: 'fit-content',
...SHARED_THEMED_CHIP_SMALL_STYLE,
...getStatusChipStyle(status),
}}
/>
<Tooltip title={isRemovedStatus(status) ? describeRow(video) : ''} enterTouchDelay={0}>
<Chip
icon={getStatusIcon(status)}
label={statusLabel}
size="small"
color={getStatusColor(status)}
variant={getStatusChipVariant(status)}
style={{
flex: '0 0 auto',
minWidth: 'fit-content',
...SHARED_THEMED_CHIP_SMALL_STYLE,
...getStatusChipStyle(status),
}}
/>
</Tooltip>
</div>
</div>
</div>
Expand Down
31 changes: 17 additions & 14 deletions client/src/components/ChannelPage/VideoListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { ChannelVideo } from '../../types/ChannelVideo';
import { decodeHtml, formatAddedDate } from '../../utils/formatters';
import { SHARED_STATUS_CHIP_SMALL_STYLE, SHARED_THEMED_CHIP_SMALL_STYLE } from '../shared/chipStyles';
import { getPublishedDateDisplay } from './publishedDateDisplay';
import { getVideoStatus, getStatusColor, getStatusIcon, getStatusLabel, getMediaTypeInfo, getStatusChipVariant, getStatusChipStyle } from '../../utils/videoStatus';
import { getVideoStatus, getStatusColor, getStatusIcon, getStatusLabel, getMediaTypeInfo, getStatusChipVariant, getStatusChipStyle, isRemovedStatus } from '../../utils/videoStatus';
import { describeRow } from '../../utils/videoRemoval';
import StillLiveDot from './StillLiveDot';
import DownloadFormatIndicator from '../shared/DownloadFormatIndicator';
import ProtectionShieldButton from '../shared/ProtectionShieldButton';
Expand Down Expand Up @@ -49,7 +50,7 @@ function VideoListItem({
const status = getVideoStatus(video);
// Check if video is still live (not "was_live" and not null/undefined)
const isStillLive = video.live_status && video.live_status !== 'was_live';
const isDownloadSelectable = (status === 'never_downloaded' || status === 'missing' || status === 'ignored') && !isStillLive;
const isDownloadSelectable = (status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored') && !isStillLive;
const isDeleteSelectable = video.added && !video.removed && !isStillLive;
const isDownloadAllowed = selectionMode !== 'delete';
const isDeleteAllowed = selectionMode !== 'download';
Expand Down Expand Up @@ -370,18 +371,20 @@ function VideoListItem({
style={{ ...SHARED_STATUS_CHIP_SMALL_STYLE }}
/>
<WatchedChip watchedBy={video.watchedBy || []} />
<Chip
icon={getStatusIcon(status)}
label={statusLabel}
size="small"
color={getStatusColor(status)}
variant={getStatusChipVariant(status)}
style={{
flex: '0 0 auto',
...SHARED_THEMED_CHIP_SMALL_STYLE,
...getStatusChipStyle(status),
}}
/>
<Tooltip title={isRemovedStatus(status) ? describeRow(video) : ''} enterTouchDelay={0}>
<Chip
icon={getStatusIcon(status)}
label={statusLabel}
size="small"
color={getStatusColor(status)}
variant={getStatusChipVariant(status)}
style={{
flex: '0 0 auto',
...SHARED_THEMED_CHIP_SMALL_STYLE,
...getStatusChipStyle(status),
}}
/>
</Tooltip>
</div>
</CardContent>
</Card>
Expand Down
28 changes: 16 additions & 12 deletions client/src/components/ChannelPage/VideoTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {
Checkbox,
Typography,
Chip,
Tooltip,
} from '../ui';
import { ArrowUpward as ArrowUpwardIcon, ArrowDownward as ArrowDownwardIcon, Block as BlockIcon, CheckCircleOutline as CheckCircleOutlineIcon, Delete as DeleteIcon } from '../../lib/icons';
import ProtectionShieldButton from '../shared/ProtectionShieldButton';
import ThumbnailClickOverlay from '../shared/ThumbnailClickOverlay';
import { formatDuration } from '../../utils';
import { ChannelVideo } from '../../types/ChannelVideo';
import { decodeHtml, formatAddedDateParts } from '../../utils/formatters';
import { getVideoStatus, getStatusColor, getStatusIcon, getStatusLabel, getMediaTypeInfo, getStatusChipVariant, getStatusChipStyle } from '../../utils/videoStatus';
import { getVideoStatus, getStatusColor, getStatusIcon, getStatusLabel, getMediaTypeInfo, getStatusChipVariant, getStatusChipStyle, isRemovedStatus } from '../../utils/videoStatus';
import { describeRow } from '../../utils/videoRemoval';
import StillLiveDot from './StillLiveDot';
import RatingBadge from '../shared/RatingBadge';
import DownloadFormatIndicator from '../shared/DownloadFormatIndicator';
Expand Down Expand Up @@ -64,7 +66,7 @@ function VideoTableView({
if (isDeleteMode) {
return video.added && !video.removed && !isStillLive;
}
return (status === 'never_downloaded' || status === 'missing' || status === 'ignored') && !video.youtube_removed && !isStillLive;
return (status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored') && !video.youtube_removed && !isStillLive;
});

return (
Expand Down Expand Up @@ -128,7 +130,7 @@ function VideoTableView({
const status = getVideoStatus(video);
const statusLabel = status === 'downloaded' ? 'Available' : getStatusLabel(status);
const isStillLive = video.live_status && video.live_status !== 'was_live';
const isDownloadSelectable = (status === 'never_downloaded' || status === 'missing' || status === 'ignored') && !video.youtube_removed && !isStillLive;
const isDownloadSelectable = (status === 'never_downloaded' || isRemovedStatus(status) || status === 'ignored') && !video.youtube_removed && !isStillLive;
const isDeleteSelectable = video.added && !video.removed && !isStillLive;
const isDownloadAllowed = selectionMode !== 'delete';
const isDeleteAllowed = selectionMode !== 'download';
Expand Down Expand Up @@ -328,7 +330,7 @@ function VideoTableView({
/>
</td>
<td style={{ whiteSpace: 'nowrap', padding: '8px 4px' }}>
{(video.filePath || video.audioFilePath) ? (
{!video.removed && (video.filePath || video.audioFilePath) ? (
<DownloadFormatIndicator
filePath={video.filePath}
audioFilePath={video.audioFilePath}
Expand All @@ -352,14 +354,16 @@ function VideoTableView({
/>
)}
<WatchedChip watchedBy={video.watchedBy || []} />
<Chip
icon={getStatusIcon(status)}
label={statusLabel}
size="small"
color={getStatusColor(status)}
variant={getStatusChipVariant(status)}
style={{ ...SHARED_THEMED_CHIP_SMALL_STYLE, ...getStatusChipStyle(status) }}
/>
<Tooltip title={isRemovedStatus(status) ? describeRow(video) : ''} enterTouchDelay={0}>
<Chip
icon={getStatusIcon(status)}
label={statusLabel}
size="small"
color={getStatusColor(status)}
variant={getStatusChipVariant(status)}
style={{ ...SHARED_THEMED_CHIP_SMALL_STYLE, ...getStatusChipStyle(status) }}
/>
</Tooltip>
</div>
</td>
</tr>
Expand Down
10 changes: 5 additions & 5 deletions client/src/components/ChannelPage/__tests__/VideoCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('VideoCard Component', () => {
});

test('renders "Missing" status for removed video', () => {
const removedVideo = { ...mockVideo, added: true, removed: true };
const removedVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoCard {...defaultProps} video={removedVideo} />);
expect(screen.getByText('Missing')).toBeInTheDocument();
});
Expand Down Expand Up @@ -242,7 +242,7 @@ describe('VideoCard Component', () => {
});

test('renders checkbox for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoCard {...defaultProps} video={missingVideo} />);
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
Expand Down Expand Up @@ -442,7 +442,7 @@ describe('VideoCard Component', () => {
});

test('does not render delete checkbox for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoCard {...defaultProps} video={missingVideo} selectionMode="delete" />);
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -475,7 +475,7 @@ describe('VideoCard Component', () => {
});

test('renders ignore button for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoCard {...defaultProps} video={missingVideo} />);
expect(screen.getByTestId('BlockIcon')).toBeInTheDocument();
});
Expand Down Expand Up @@ -849,7 +849,7 @@ describe('VideoCard Component', () => {
});

test('renders CloudOffIcon for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoCard {...defaultProps} video={missingVideo} />);
expect(screen.getByTestId('CloudOffIcon')).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('VideoListItem Component', () => {
});

test('renders "Missing" status for removed video', () => {
const removedVideo = { ...mockVideo, added: true, removed: true };
const removedVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoListItem {...defaultProps} video={removedVideo} />);
expect(screen.getByText('Missing')).toBeInTheDocument();
});
Expand Down Expand Up @@ -238,7 +238,7 @@ describe('VideoListItem Component', () => {
});

test('renders checkbox for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoListItem {...defaultProps} video={missingVideo} />);
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
Expand Down Expand Up @@ -476,7 +476,7 @@ describe('VideoListItem Component', () => {
});

test('does not render delete checkbox for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoListItem {...defaultProps} video={missingVideo} selectionMode="delete" />);
expect(screen.queryByLabelText(/mark for deletion/i)).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -511,7 +511,7 @@ describe('VideoListItem Component', () => {
});

test('renders ignore button for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoListItem {...defaultProps} video={missingVideo} />);
expect(screen.getByTestId('BlockIcon')).toBeInTheDocument();
expect(screen.getByLabelText('Click to ignore')).toBeInTheDocument();
Expand Down Expand Up @@ -758,7 +758,7 @@ describe('VideoListItem Component', () => {
});

test('renders CloudOffIcon for missing videos', () => {
const missingVideo = { ...mockVideo, added: true, removed: true };
const missingVideo = { ...mockVideo, added: true, removed: true, removed_reason: 'not_found' as const };
renderWithProviders(<VideoListItem {...defaultProps} video={missingVideo} />);
expect(screen.getByTestId('CloudOffIcon')).toBeInTheDocument();
});
Expand Down
Loading
Loading