A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: improve show navigation and search results

- Limit command search bar to 3 results per section
- Make 'Start Watching' button navigate to the first episode (S1E1)
- Compute episode prev/next navigation client-side from season data
- Relax backend isNavigableEpisode to only exclude season 0 specials

+63 -13
+1 -1
apps/web/src/components/SearchCommand.tsx
··· 60 60 return debouncedValue; 61 61 } 62 62 63 - const RESULTS_PER_SECTION = 8; 63 + const RESULTS_PER_SECTION = 3; 64 64 65 65 export function SearchCommand({ 66 66 open: controlledOpen,
+22
apps/web/src/routes/shows/$showId/$showName/index.tsx
··· 152 152 return "Start Watching"; 153 153 }; 154 154 155 + const isStartWatching = 156 + !nextEpisode && !(isTracking && uniqueEpisodesWatched > 0); 157 + 158 + const firstSeasonNumber = 159 + show?.seasons 160 + ?.filter((s) => s.season_number > 0) 161 + .sort((a, b) => a.season_number - b.season_number)[0]?.season_number || 1; 162 + 155 163 const handleMarkSeasonWatched = (seasonNumber: number) => { 156 164 if (!isAuthenticated) return; 157 165 setProcessingSeason(seasonNumber); ··· 293 301 showName: slugifyName(show.name), 294 302 seasonNumber: String(nextEpisode.seasonNumber), 295 303 episodeNumber: String(nextEpisode.episodeNumber), 304 + }} 305 + className="btn btn-primary gap-2" 306 + > 307 + <Play className="size-4" /> 308 + {getCurrentEpisodeText()} 309 + </Link> 310 + ) : isStartWatching ? ( 311 + <Link 312 + to="/shows/$showId/$showName/seasons/$seasonNumber/episodes/$episodeNumber" 313 + params={{ 314 + showId, 315 + showName: slugifyName(show.name), 316 + seasonNumber: String(firstSeasonNumber), 317 + episodeNumber: "1", 296 318 }} 297 319 className="btn btn-primary gap-2" 298 320 >
+38 -3
apps/web/src/routes/shows/$showId/$showName/seasons.$seasonNumber.episodes.$episodeNumber.tsx
··· 174 174 : `https://i.pravatar.cc/150?u=${person.id}`, 175 175 })) || []; 176 176 177 - // Previous / Next episode navigation 178 - const prevEpisode = episode._context?.previous; 179 - const nextEpisodeCtx = episode._context?.next; 177 + // Previous / Next episode navigation (computed client-side from show data) 178 + const seasons = 179 + show.seasons 180 + ?.filter((s) => s.season_number > 0) 181 + .sort((a, b) => a.season_number - b.season_number) || []; 182 + const currentSeason = seasons.find((s) => s.season_number === seasonNum); 183 + const currentSeasonEpisodeCount = currentSeason?.episode_count || 0; 184 + 185 + let prevEpisode: { seasonNumber: number; episodeNumber: number } | null = 186 + null; 187 + if (episodeNum > 1) { 188 + prevEpisode = { seasonNumber: seasonNum, episodeNumber: episodeNum - 1 }; 189 + } else { 190 + const prevSeason = seasons.find((s) => s.season_number === seasonNum - 1); 191 + if (prevSeason) { 192 + prevEpisode = { 193 + seasonNumber: prevSeason.season_number, 194 + episodeNumber: prevSeason.episode_count || 1, 195 + }; 196 + } 197 + } 198 + 199 + let nextEpisodeCtx: { seasonNumber: number; episodeNumber: number } | null = 200 + null; 201 + if (episodeNum < currentSeasonEpisodeCount) { 202 + nextEpisodeCtx = { 203 + seasonNumber: seasonNum, 204 + episodeNumber: episodeNum + 1, 205 + }; 206 + } else { 207 + const nextSeason = seasons.find((s) => s.season_number === seasonNum + 1); 208 + if (nextSeason) { 209 + nextEpisodeCtx = { 210 + seasonNumber: nextSeason.season_number, 211 + episodeNumber: 1, 212 + }; 213 + } 214 + } 180 215 181 216 const breadcrumbs = [ 182 217 {
+2 -9
backend/src/shows/shows-tmdb.service.ts
··· 328 328 } 329 329 330 330 private isNavigableEpisode(episode: TMDBEpisode): boolean { 331 - if (episode.season_number === 0) { 332 - return false; 333 - } 334 - 335 - if (!episode.air_date) { 336 - return false; 337 - } 338 - 339 - return new Date(episode.air_date).getTime() <= Date.now(); 331 + // Exclude specials / bonus content (season 0) 332 + return episode.season_number !== 0; 340 333 } 341 334 }