mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
Rename TMDB_API_KEY to TMDB_API_READ_ACCESS_TOKEN and add optional base URL overrides
Renames the env var for clarity since it holds a read access token, not an API key. Adds optional TMDB_API_BASE_URL and TMDB_IMAGE_BASE_URL env vars for advanced users. Centralizes image URL construction into a shared tmdbImageUrl() helper, replacing hardcoded URLs across all components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+6
-2
@@ -1,11 +1,15 @@
|
||||
# SQLite database URL (Docker: file:/data/sqlite.db, local dev: file:sqlite.db)
|
||||
DATABASE_URL=file:sqlite.db
|
||||
|
||||
# TMDB API key — get one at https://www.themoviedb.org/settings/api
|
||||
TMDB_API_KEY=your_tmdb_api_key_here
|
||||
# TMDB API Read Access Token — get one at https://www.themoviedb.org/settings/api
|
||||
TMDB_API_READ_ACCESS_TOKEN=your_tmdb_api_read_access_token_here
|
||||
|
||||
# Random secret for session encryption (min 32 chars)
|
||||
BETTER_AUTH_SECRET=your_secret_here
|
||||
|
||||
# Public URL of your instance
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Optional: override TMDB base URLs (advanced)
|
||||
# TMDB_API_BASE_URL=https://api.themoviedb.org/3
|
||||
# TMDB_IMAGE_BASE_URL=https://image.tmdb.org/t/p
|
||||
|
||||
@@ -77,7 +77,7 @@ All app tables use UUID text primary keys generated via the `uuid` package. Bett
|
||||
|
||||
### TMDB images
|
||||
|
||||
Only paths are stored in DB. Full URLs are constructed at render time: `https://image.tmdb.org/t/p/{size}{path}` (common sizes: w300, w500, w1280).
|
||||
Only paths are stored in DB. Full URLs are constructed at render time via `tmdbImageUrl()` from `lib/tmdb/image.ts` using the `TMDB_IMAGE_BASE_URL` env var (defaults to `https://image.tmdb.org/t/p`). Common sizes: w300, w500, w1280.
|
||||
|
||||
### Background jobs
|
||||
|
||||
@@ -85,7 +85,7 @@ Registered in `lib/jobs/registry.ts`, started via Next.js instrumentation hook (
|
||||
|
||||
### Environment variables
|
||||
|
||||
See `.env.example`: `DATABASE_URL`, `TMDB_API_KEY`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`.
|
||||
See `.env.example`: `DATABASE_URL`, `TMDB_API_READ_ACCESS_TOKEN`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`.
|
||||
|
||||
## Browser Automation
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { DashboardSkeleton } from "@/components/skeletons";
|
||||
import { StatsSummary } from "@/components/stats-summary";
|
||||
import { TitleCard } from "@/components/title-card";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
import { useSession } from "@/lib/auth/client";
|
||||
|
||||
interface ContinueWatchingItem {
|
||||
@@ -270,11 +271,9 @@ function FeedSection({
|
||||
}
|
||||
|
||||
function ContinueWatchingCard({ item }: { item: ContinueWatchingItem }) {
|
||||
const stillUrl = item.nextEpisode?.stillPath
|
||||
? `https://image.tmdb.org/t/p/w500${item.nextEpisode.stillPath}`
|
||||
: item.title.backdropPath
|
||||
? `https://image.tmdb.org/t/p/w500${item.title.backdropPath}`
|
||||
: null;
|
||||
const stillUrl =
|
||||
tmdbImageUrl(item.nextEpisode?.stillPath ?? null, "w500") ??
|
||||
tmdbImageUrl(item.title.backdropPath ?? null, "w500");
|
||||
const progress =
|
||||
item.totalEpisodes > 0
|
||||
? (item.watchedEpisodes / item.totalEpisodes) * 100
|
||||
|
||||
@@ -57,23 +57,23 @@ const steps = [
|
||||
{
|
||||
number: "3",
|
||||
title: "Add it to your environment",
|
||||
description: "Set the TMDB_API_KEY environment variable and restart Sofa.",
|
||||
description: "Set the TMDB_API_READ_ACCESS_TOKEN environment variable and restart Sofa.",
|
||||
},
|
||||
];
|
||||
|
||||
const envSnippets = [
|
||||
{
|
||||
label: ".env file",
|
||||
code: "TMDB_API_KEY=your_api_read_access_token_here",
|
||||
code: "TMDB_API_READ_ACCESS_TOKEN=your_api_read_access_token_here",
|
||||
},
|
||||
{
|
||||
label: "Docker Compose",
|
||||
code: `environment:
|
||||
- TMDB_API_KEY=your_api_read_access_token_here`,
|
||||
- TMDB_API_READ_ACCESS_TOKEN=your_api_read_access_token_here`,
|
||||
},
|
||||
{
|
||||
label: "Docker run",
|
||||
code: "docker run -e TMDB_API_KEY=your_token ...",
|
||||
code: "docker run -e TMDB_API_READ_ACCESS_TOKEN=your_token ...",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { TitleDetailSkeleton } from "@/components/skeletons";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
import { StarRating } from "@/components/star-rating";
|
||||
import { StatusButton } from "@/components/status-button";
|
||||
import { TitleCard } from "@/components/title-card";
|
||||
@@ -355,12 +356,8 @@ export default function TitleDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const posterUrl = title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w500${title.posterPath}`
|
||||
: null;
|
||||
const backdropUrl = title.backdropPath
|
||||
? `https://image.tmdb.org/t/p/w1280${title.backdropPath}`
|
||||
: null;
|
||||
const posterUrl = tmdbImageUrl(title.posterPath, "w500");
|
||||
const backdropUrl = tmdbImageUrl(title.backdropPath, "w1280");
|
||||
const dateStr = title.releaseDate ?? title.firstAirDate;
|
||||
const year = dateStr?.slice(0, 4);
|
||||
|
||||
@@ -696,9 +693,7 @@ export default function TitleDetailPage() {
|
||||
const isWatched = title.episodeWatches?.includes(
|
||||
ep.id,
|
||||
);
|
||||
const stillUrl = ep.stillPath
|
||||
? `https://image.tmdb.org/t/p/w300${ep.stillPath}`
|
||||
: null;
|
||||
const stillUrl = tmdbImageUrl(ep.stillPath, "w300");
|
||||
return (
|
||||
<div
|
||||
key={ep.id}
|
||||
@@ -818,7 +813,7 @@ function ProviderBadge({
|
||||
name: string;
|
||||
logoPath: string | null;
|
||||
}) {
|
||||
const logoUrl = logoPath ? `https://image.tmdb.org/t/p/w92${logoPath}` : null;
|
||||
const logoUrl = tmdbImageUrl(logoPath, "w92");
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useSession } from "@/lib/auth/client";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
|
||||
/** Check whether the server has TMDB configured. */
|
||||
async function fetchSetupStatus(): Promise<boolean> {
|
||||
@@ -103,7 +104,7 @@ export default function Home() {
|
||||
>
|
||||
<div className="overflow-hidden rounded-xl shadow-lg">
|
||||
<Image
|
||||
src={`https://image.tmdb.org/t/p/w300${path}`}
|
||||
src={tmdbImageUrl(path, "w300")!}
|
||||
alt=""
|
||||
width={300}
|
||||
height={450}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import Image from "next/image";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useKeyboard } from "@/components/keyboard-provider";
|
||||
@@ -211,7 +212,7 @@ export function CommandPalette() {
|
||||
<div className="h-12 w-8 shrink-0 overflow-hidden rounded bg-muted">
|
||||
{r.posterPath ? (
|
||||
<Image
|
||||
src={`https://image.tmdb.org/t/p/w92${r.posterPath}`}
|
||||
src={tmdbImageUrl(r.posterPath, "w92")!}
|
||||
alt={r.title}
|
||||
width={32}
|
||||
height={48}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
|
||||
interface SearchResult {
|
||||
tmdbId: number;
|
||||
@@ -165,7 +166,7 @@ export function SearchAutocomplete({
|
||||
<div className="h-[60px] w-10 shrink-0 overflow-hidden rounded-md bg-muted">
|
||||
{r.posterPath ? (
|
||||
<Image
|
||||
src={`https://image.tmdb.org/t/p/w92${r.posterPath}`}
|
||||
src={tmdbImageUrl(r.posterPath, "w92")!}
|
||||
alt={r.title}
|
||||
width={40}
|
||||
height={60}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { tmdbImageUrl } from "@/lib/tmdb/image";
|
||||
|
||||
interface TitleCardProps {
|
||||
id?: string;
|
||||
@@ -27,9 +28,7 @@ export function TitleCard({
|
||||
onImport,
|
||||
}: TitleCardProps) {
|
||||
const year = releaseDate?.slice(0, 4);
|
||||
const posterUrl = posterPath
|
||||
? `https://image.tmdb.org/t/p/w300${posterPath}`
|
||||
: null;
|
||||
const posterUrl = tmdbImageUrl(posterPath, "w300");
|
||||
|
||||
const content = (
|
||||
<motion.div
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ services:
|
||||
- sofa-data:/data
|
||||
environment:
|
||||
- DATABASE_URL=file:/data/sqlite.db
|
||||
- TMDB_API_KEY=${TMDB_API_KEY}
|
||||
- TMDB_API_READ_ACCESS_TOKEN=${TMDB_API_READ_ACCESS_TOKEN}
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:3000}
|
||||
healthcheck:
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@
|
||||
*/
|
||||
|
||||
export function isTmdbConfigured(): boolean {
|
||||
return !!process.env.TMDB_API_KEY;
|
||||
return !!process.env.TMDB_API_READ_ACCESS_TOKEN;
|
||||
}
|
||||
|
||||
+5
-8
@@ -7,11 +7,11 @@ import type {
|
||||
TmdbWatchProviderResponse,
|
||||
} from "./types";
|
||||
|
||||
const BASE_URL = "https://api.themoviedb.org/3";
|
||||
|
||||
const BASE_URL =
|
||||
process.env.TMDB_API_BASE_URL || "https://api.themoviedb.org/3";
|
||||
function getApiKey() {
|
||||
const key = process.env.TMDB_API_KEY;
|
||||
if (!key) throw new Error("TMDB_API_KEY is not set");
|
||||
const key = process.env.TMDB_API_READ_ACCESS_TOKEN;
|
||||
if (!key) throw new Error("TMDB_API_READ_ACCESS_TOKEN is not set");
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,4 @@ export async function getSimilar(tmdbId: number, type: "movie" | "tv") {
|
||||
return tmdbFetch<TmdbRecommendationResponse>(`/${type}/${tmdbId}/similar`);
|
||||
}
|
||||
|
||||
export function tmdbImageUrl(path: string | null, size = "w500") {
|
||||
if (!path) return null;
|
||||
return `https://image.tmdb.org/t/p/${size}${path}`;
|
||||
}
|
||||
export { tmdbImageUrl } from "./image";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const IMAGE_BASE_URL =
|
||||
process.env.TMDB_IMAGE_BASE_URL || "https://image.tmdb.org/t/p";
|
||||
|
||||
export function tmdbImageUrl(path: string | null, size = "w500") {
|
||||
if (!path) return null;
|
||||
return `${IMAGE_BASE_URL}/${size}${path}`;
|
||||
}
|
||||
+9
-2
@@ -1,14 +1,21 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const imageBaseUrl = process.env.TMDB_IMAGE_BASE_URL || "";
|
||||
const imageHost = imageBaseUrl
|
||||
? new URL(imageBaseUrl).hostname
|
||||
: "image.tmdb.org";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactCompiler: true,
|
||||
env: {
|
||||
TMDB_IMAGE_BASE_URL: process.env.TMDB_IMAGE_BASE_URL || "",
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "image.tmdb.org",
|
||||
pathname: "/t/p/**",
|
||||
hostname: imageHost,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user