Skip to main content
The watch progress system provides hooks for tracking video playback, managing episode watch states, and syncing progress across devices.

useWatchProgress

Retrieves the current watch progress for a specific media item.

Usage

import { useWatchProgress } from '@/hooks/useWatchProgress';

function ResumeWatchingCard({ movieId }) {
  const { progress } = useWatchProgress(movieId, 'movie');

  if (!progress || progress.percent === 0) return null;

  return (
    <div>
      <h3>Resume Watching</h3>
      <ProgressBar value={progress.percent} />
      <p>{progress.percent}% complete</p>
    </div>
  );
}

Parameters

id
string | number
required
TMDB ID of the media item
mediaType
'movie' | 'tv'
required
Type of media content

Return Values

progress
WatchProgressData | null
Watch progress information, or null if no progress exists

useContinueWatching

Retrieves all in-progress items for a “Continue Watching” section.

Usage

import { useContinueWatching } from '@/hooks/useWatchProgress';

function ContinueWatchingSection() {
  const { items } = useContinueWatching();

  if (items.length === 0) return null;

  return (
    <section>
      <h2>Continue Watching</h2>
      <div className="grid">
        {items.map((item) => (
          <MediaCard 
            key={item.id}
            id={item.id}
            type={item.type}
            progress={item.percent}
          />
        ))}
      </div>
    </section>
  );
}

Return Values

items
WatchProgressData[]
Array of items with progress between 0% and 100% (exclusive)
allItems
WatchProgressData[]
Same as items (legacy alias)

useEpisodeWatched

Comprehensive hook for managing TV show episode watch states with automatic progress synchronization.

Usage

import { useEpisodeWatched } from '@/hooks/useWatchProgress';

function EpisodeBrowser({ tvId, seasons, showMeta }) {
  const totalEpisodes = seasons.reduce((acc, s) => acc + s.episode_count, 0);
  
  const episodeTracker = useEpisodeWatched(tvId, totalEpisodes, {
    title: showMeta.name,
    image: showMeta.poster_path,
    release_date: showMeta.first_air_date,
    overview: showMeta.overview,
    rating: showMeta.vote_average,
    status: showMeta.status,
  });

  return (
    <div>
      <p>Watched: {episodeTracker.watchedCount} / {totalEpisodes}</p>
      
      {seasons.map((season) => (
        <SeasonCard
          key={season.season_number}
          season={season}
          tracker={episodeTracker}
        />
      ))}
    </div>
  );
}

Parameters

tvId
number | string
required
TMDB ID of the TV show
totalEpisodes
number
default:"undefined"
Total episode count across all seasons (for progress calculation)
showMeta
object
default:"{}"
TV show metadata for progress status updates

Return Values

isEpisodeWatched
(season: number, episode: number) => boolean
Function to check if a specific episode is marked as watched
toggleEpisodeWatched
(season: number, episode: number) => void
Toggle watch status for a single episode. Automatically syncs progress percentage and status.
markSeasonWatched
(season: number, episodes: number[]) => void
Mark all episodes in a season as watched. Accepts array of episode numbers.
unmarkSeasonWatched
(season: number, episodes: number[]) => void
Unmark all episodes in a season. Accepts array of episode numbers.
isSeasonFullyWatched
(season: number, totalEpisodesCount: number) => boolean
Check if all episodes in a season are marked as watched
getSeasonWatchedCount
(season: number, totalEpisodesCount: number) => number
Get the count of watched episodes in a specific season
markShowCompleted
(totalEpisodesOverride: number) => void
Mark the entire show as completed (sets progress to 100% and status to “finished”)
watchedCount
number
Total number of episodes marked as watched across all seasons

Real-World Example

import { useEpisodeWatched } from '@/hooks/useWatchProgress';

function SeasonCard({ season, tracker, tvId }) {
  const { season_number, episode_count } = season;
  
  const seenAll = tracker.isSeasonFullyWatched(season_number, episode_count);
  const watchedCount = tracker.getSeasonWatchedCount(season_number, episode_count);
  const episodeNumbers = Array.from({ length: episode_count }, (_, i) => i + 1);

  const handleSeasonToggle = () => {
    if (seenAll) {
      tracker.unmarkSeasonWatched(season_number, episodeNumbers);
    } else {
      tracker.markSeasonWatched(season_number, episodeNumbers);
    }
  };

  return (
    <div>
      <div>
        <h3>Season {season_number}</h3>
        <span>{watchedCount}/{episode_count} watched</span>
        <button onClick={handleSeasonToggle}>
          {seenAll ? 'Unmark Season' : 'Mark Season Watched'}
        </button>
      </div>
      
      {episodeNumbers.map(ep => (
        <EpisodeRow
          key={ep}
          episode={ep}
          isWatched={tracker.isEpisodeWatched(season_number, ep)}
          onToggle={() => tracker.toggleEpisodeWatched(season_number, ep)}
        />
      ))}
    </div>
  );
}

useEpisodeProgress

Get watch progress for a specific episode.

Usage

import { useEpisodeProgress } from '@/hooks/useWatchProgress';

function EpisodeCard({ tvId, season, episode }) {
  const progress = useEpisodeProgress(tvId, season, episode);
  
  return (
    <div>
      <h4>S{season}E{episode}</h4>
      {progress === 100 && <CheckIcon />}
    </div>
  );
}

Parameters

tvId
string | number
required
TMDB ID of the TV show
season
number
required
Season number
episode
number
required
Episode number

Return Value

Returns 100 if the episode is watched, 0 otherwise.

usePlayerProgressListener

Listens for player events via postMessage and automatically persists progress.

Usage

import { usePlayerProgressListener } from '@/hooks/useWatchProgress';

function App() {
  usePlayerProgressListener();
  
  return <Router />;
}

Implementation Notes

  • Listens for PLAYER_EVENT messages from embedded video player
  • Automatically saves progress every 2% change
  • Auto-marks episodes as watched at 95% completion
  • Syncs with Convex for authenticated users, localStorage for guests
  • Debounces updates to avoid excessive API calls

Player Event Format

window.postMessage(JSON.stringify({
  type: 'PLAYER_EVENT',
  data: {
    event: 'timeupdate', // or 'play', 'pause', 'ended', 'seeked'
    currentTime: 120,
    duration: 3600,
    progress: 33.3,
    id: '12345',
    mediaType: 'tv',
    season: 1,
    episode: 5,
  },
}), '*');

buildPlayerUrl

Utility function to construct embedded player URLs with progress restoration.

Usage

import { buildPlayerUrl } from '@/hooks/useWatchProgress';

function WatchButton({ movie, progress }) {
  const playerUrl = buildPlayerUrl({
    type: 'movie',
    tmdbId: movie.id,
    savedProgress: progress?.percent,
  });
  
  return <a href={playerUrl}>Watch Now</a>;
}

Parameters

type
'movie' | 'tv'
required
Media type
tmdbId
number
required
TMDB ID
season
number
Season number (for TV shows)
episode
number
Episode number (for TV shows)
savedProgress
number
Saved progress percentage. Only applied if > 10%

Return Value

Returns a complete player URL with query parameters for autoplay, next episode navigation, and progress restoration.

Source

Location: ~/workspace/source/src/hooks/useWatchProgress.ts Progress tracking integrates with both Convex backend and local storage via Zustand.

Build docs developers (and LLMs) love