[
  {
    "path": ".env.example",
    "content": "# Database\nDATABASE_URL=\"mysql://user:password@localhost:3306/streamium\"\n\n# Authentication (REQUIRED - use a strong random secret, min 32 chars)\n# Generate with: openssl rand -base64 32\nJWT_SECRET=\"your-secure-jwt-secret-min-32-characters\"\n\n# TMDB API (REQUIRED)\nTMDB_API_KEY=\"your-tmdb-api-key\"\nTMDB_API_URL=\"https://api.themoviedb.org/3\"\nTMDB_IMAGE_URL=\"https://image.tmdb.org/t/p\"\n\n# Streaming Providers\nVIDSRC_BASE_URL=\"https://vidsrc.cc/v2/embed\"\nVIDLINK_BASE_URL=\"https://vidlink.pro\"\nMOVIES111_BASE_URL=\"https://111movies.com\"\nEMBED2_BASE_URL=\"https://www.2embed.cc\"\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "ko_fi: kasterby\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# Environment\n.env\n.env.*\n!.env.example\n\n# Dependencies\nnode_modules\npackage-lock.json\n\n# Build output\n.svelte-kit\nbuild\ndist\n\n# Database\n*.db\n*.db-journal\nprisma/dev.db\nprisma/migrations\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# OS\n.DS_Store\nThumbs.db\n\n# Cache\n.cache\nstatic/image-cache/\n\n# Testing\ncoverage\n.nyc_output\n\n# Temporary files\n*.tmp\n*.temp\n"
  },
  {
    "path": ".npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Streamium\n\nThank you for your interest in contributing to Streamium! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n1. Fork the repository\n2. Clone your fork: `git clone https://github.com/gmonarque/streamium.git`\n3. Create a new branch: `git checkout -b feature/your-feature-name`\n4. Copy `.env.example` to `.env` and configure your environment variables\n5. Install dependencies: `npm install`\n6. Initialize the database: `npx prisma migrate dev`\n7. Start the development server: `npm run dev`\n\n## Development Guidelines\n\n### Code Style\n\n- Use TypeScript for type safety\n- Follow the existing code style and formatting\n- Use meaningful variable and function names\n- Keep functions small and focused\n- Add comments only when necessary to explain complex logic\n\n### Commit Messages\n\n- Use clear and descriptive commit messages\n- Start with a verb in present tense (e.g., \"Add\", \"Fix\", \"Update\")\n- Reference issue numbers when applicable\n\nExample:\n```\nAdd password strength validation\nFix rate limiting on login endpoint\nUpdate user profile UI components\n```\n\n### Pull Requests\n\n1. Update your branch with the latest main branch\n2. Ensure all tests pass\n3. Update documentation if needed\n4. Create a pull request with a clear description of changes\n5. Link any related issues\n\n### Testing\n\n- Write tests for new features\n- Ensure existing tests pass\n- Test your changes in different browsers\n- Check mobile responsiveness\n\n### Security\n\n- Follow security guidelines in SECURITY.md\n- Never commit sensitive data\n- Use environment variables for secrets\n- Implement rate limiting for new endpoints\n- Validate and sanitize all user input\n\n## Project Structure\n\n```\nstreamium/\n├── src/\n│   ├── lib/          # Components, services, stores\n│   ├── routes/       # SvelteKit routes and API\n│   └── app.html      # App template\n├── prisma/\n│   └── schema.prisma # Database schema\n└── static/           # Static assets\n```\n\n## Need Help?\n\n- Check existing issues and pull requests\n- Read the documentation\n- Ask questions in discussions\n- Follow the code of conduct\n\n## Code of Conduct\n\n- Be respectful and inclusive\n- No harassment or discrimination\n- Constructive feedback only\n- Follow project maintainers' decisions\n\nThank you for contributing to Streamium!\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:18-slim\n\nWORKDIR /app\n\nRUN apt-get update && apt-get install -y \\\n    openssl \\\n    default-mysql-client \\\n    && rm -rf /var/lib/apt/lists/*\n\nRUN corepack enable\n\nCOPY package.json pnpm-lock.yaml ./\nCOPY prisma ./prisma/\n\nRUN pnpm install --frozen-lockfile\n\nCOPY . .\n\nRUN pnpm prisma generate\nRUN pnpm run build\nRUN pnpm prune --prod\n\nEXPOSE 5173\n\nENV NODE_ENV=production\n\nCMD [\"node\", \"build\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 https://github.com/gmonarque\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Streamium\n\nA SvelteKit streaming UI that embeds content from third-party providers and uses TMDB for movie/TV metadata.\n\n<p>\n  <img src=\"./screenshots/screenshot1.png\" width=\"320\" alt=\"Streamium screenshot 1\" />\n  <img src=\"./screenshots/screenshot2.png\" width=\"320\" alt=\"Streamium screenshot 2\" />\n  <img src=\"./screenshots/screenshot3.png\" width=\"320\" alt=\"Streamium screenshot 3\" />\n</p>\n\n## Features\n- TMDB-powered catalog, search, and filters\n- Multiple embed providers (VidSrc, VidLink, 111Movies, 2Embed)\n- Auth, watchlist, and comments with moderation\n- Server-rendered UI with image proxying\n- Basic security controls (CSRF, captcha, rate limiting, CSP)\n\n## Tech Stack\n- SvelteKit + TypeScript + Tailwind CSS\n- Prisma + MySQL\n- JWT auth, Zod validation\n\n## Quickstart (Local)\n\nPrereqs: Node.js 18+, pnpm, and MySQL (optional if you only browse content).\n\n1) Configure env:\n```bash\ncp .env.example .env\n```\n\n2) Install deps:\n```bash\npnpm install\n```\n\n3) (Optional) DB setup:\n```bash\npnpm prisma generate\npnpm prisma migrate dev\n```\n\n4) Run dev server:\n```bash\npnpm dev\n```\n\nApp runs at http://localhost:5173\n\n## Docker\n\n1) Set required env vars (at minimum):\n- `JWT_SECRET`\n- `TMDB_API_KEY`\n- `MYSQL_PASSWORD`\n- `MYSQL_ROOT_PASSWORD`\n\n2) Start:\n```bash\ndocker compose up --build\n```\n\n3) Run migrations:\n```bash\ndocker compose exec web pnpm prisma migrate dev\n```\n\n## Environment Variables\n\nKey entries in `.env.example`:\n- `DATABASE_URL`\n- `JWT_SECRET`\n- `TMDB_API_KEY`\n- `TMDB_API_URL`\n- Provider URLs: `VIDSRC_BASE_URL`, `VIDLINK_BASE_URL`, `MOVIES111_BASE_URL`, `EMBED2_BASE_URL`\n\n## Scripts\n- `pnpm dev` – start dev server\n- `pnpm build` – production build\n- `pnpm test` – run tests\n\n## License\nMIT\n\n## Legal Disclaimer\nThis project embeds third‑party content and does not host media files. Use it only with properly licensed content and in compliance with applicable laws. The software is provided “as is” without warranty.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf you discover a security vulnerability within Streamium, please send a private message on github. All security vulnerabilities will be promptly addressed.\n\nPlease include the following information in your report:\n- Description of the vulnerability\n- Steps to reproduce the issue\n- Potential impact\n- Any suggested fixes (if applicable)\n\n## Security Measures\n\nStreamium implements several security measures:\n\n1. **Authentication**\n   - JWT-based authentication\n   - Secure password hashing\n   - Rate limiting on login attempts\n   - Password reset with secure tokens\n\n2. **Data Protection**\n   - Input validation and sanitization\n   - XSS protection\n   - CSRF protection\n   - SQL injection prevention through Prisma ORM\n\n3. **API Security**\n   - Rate limiting on sensitive endpoints\n   - Request validation\n   - Secure error handling\n\n## Development Guidelines\n\nWhen contributing to Streamium, please ensure:\n\n1. All passwords are hashed using bcrypt\n2. Sensitive data is never logged\n3. Environment variables are used for secrets\n4. Input is properly validated and sanitized\n5. Rate limiting is implemented on sensitive endpoints\n6. Error messages don't leak sensitive information\n\n## Known Issues\n\nThere are currently no known security issues. Check this section for updates on security-related issues and their status.\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  web:\n    build: .\n    ports:\n      - \"5173:5173\"\n    volumes:\n      - .:/app\n      - /app/node_modules\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-mysql://root:rootpassword@db:3306/streamium}\n      - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}\n      - TMDB_API_KEY=${TMDB_API_KEY:?TMDB_API_KEY is required}\n      - TMDB_API_URL=https://api.themoviedb.org/3\n      - TMDB_IMAGE_URL=https://image.tmdb.org/t/p\n      - VIDSRC_BASE_URL=https://vidsrc.cc/v2/embed\n      - VIDSRC_PRO_BASE_URL=https://vidsrc.pro/embed\n      - EMBEDSU_BASE_URL=https://embed.su/embed\n      - NODE_ENV=${NODE_ENV:-development}\n    depends_on:\n      - db\n\n  db:\n    image: mysql:8\n    ports:\n      - \"3307:3306\"\n    environment:\n      - MYSQL_DATABASE=streamium\n      - MYSQL_USER=${MYSQL_USER:-user}\n      - MYSQL_PASSWORD=${MYSQL_PASSWORD:?MYSQL_PASSWORD is required}\n      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD is required}\n    volumes:\n      - mysql_data:/var/lib/mysql\n      - ./init.sql:/docker-entrypoint-initdb.d/init.sql\n\nvolumes:\n  mysql_data:\n"
  },
  {
    "path": "init.sql",
    "content": "GRANT CREATE ON *.* TO 'user'@'%';\nCREATE DATABASE IF NOT EXISTS `streamium_shadow`;\nGRANT ALL PRIVILEGES ON `streamium_shadow`.* TO 'user'@'%';\nGRANT ALL PRIVILEGES ON `streamium`.* TO 'user'@'%';\nFLUSH PRIVILEGES;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"streamium\",\n  \"version\": \"0.0.1\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite dev\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"test\": \"vitest run\",\n    \"check\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\",\n    \"check:watch\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch\",\n    \"start\": \"node server.js\"\n  },\n  \"devDependencies\": {\n    \"@sveltejs/adapter-auto\": \"^3.3.1\",\n    \"@sveltejs/vite-plugin-svelte\": \"^4.0.0\",\n    \"@types/bcryptjs\": \"^2.4.6\",\n    \"@types/node\": \"^22.9.0\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"postcss\": \"^8.4.49\",\n    \"svelte-check\": \"^4.0.0\",\n    \"tailwindcss\": \"^3.4.14\",\n    \"typescript\": \"^5.0.0\",\n    \"vite\": \"^5.4.11\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"dependencies\": {\n    \"@emoji-mart/data\": \"^1.2.1\",\n    \"@emoji-mart/react\": \"^1.1.1\",\n    \"@melt-ui/pp\": \"^0.3.2\",\n    \"@prisma/client\": \"5.22.0\",\n    \"@sveltejs/adapter-node\": \"^5.2.9\",\n    \"@sveltejs/kit\": \"^2.8.0\",\n    \"@tailwindcss/aspect-ratio\": \"^0.4.2\",\n    \"@tailwindcss/forms\": \"^0.5.9\",\n    \"@tailwindcss/typography\": \"^0.5.15\",\n    \"@tiptap/core\": \"^2.9.1\",\n    \"@tiptap/extension-link\": \"^2.9.1\",\n    \"@tiptap/starter-kit\": \"^2.9.1\",\n    \"@tiptap/suggestion\": \"^2.9.1\",\n    \"@types/dompurify\": \"^3.0.5\",\n    \"@types/jsonwebtoken\": \"^9.0.7\",\n    \"@types/sharp\": \"^0.31.1\",\n    \"bcryptjs\": \"^3.0.3\",\n    \"date-fns\": \"^4.1.0\",\n    \"dompurify\": \"^3.2.0\",\n    \"emoji-mart\": \"^5.6.0\",\n    \"isomorphic-dompurify\": \"^2.17.0\",\n    \"jsonwebtoken\": \"^9.0.2\",\n    \"mysql2\": \"^3.11.4\",\n    \"prisma\": \"5.22.0\",\n    \"sharp\": \"^0.33.5\",\n    \"svelte\": \"^5.0.0\",\n    \"svelte-preprocess\": \"^6.0.3\",\n    \"svelte-sequential-preprocessor\": \"^2.0.2\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"cookie\": \"0.7.2\"\n    }\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "prisma/schema.prisma",
    "content": "generator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"mysql\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel User {\n  id              Int           @id @default(autoincrement())\n  username        String        @unique\n  email           String?       @unique\n  passwordHash    String\n  resetToken      String?\n  resetTokenExp   DateTime?\n  isAdmin         Boolean       @default(false)\n  createdAt       DateTime      @default(now())\n  updatedAt       DateTime      @updatedAt\n  watchlist       Watchlist[]\n  comments        Comment[]\n  commentLikes    CommentLike[]\n\n  @@map(\"users\")\n}\n\nmodel Watchlist {\n  id          Int      @id @default(autoincrement())\n  userId      Int\n  mediaId     Int\n  mediaType   String\n  title       String\n  posterPath  String?\n  voteAverage Float    @default(0)\n  addedAt     DateTime @default(now())\n  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@unique([userId, mediaId, mediaType])\n  @@map(\"watchlist\")\n}\n\nmodel Comment {\n  id          Int           @id @default(autoincrement())\n  userId      Int\n  mediaId     Int\n  mediaType   String\n  season      Int?\n  episode     Int?\n  content     String       @db.Text\n  createdAt   DateTime     @default(now())\n  updatedAt   DateTime     @updatedAt\n  parentId    Int?\n  flagged     Boolean      @default(false)\n  flagReason  String?      @db.Text\n  flaggedAt   DateTime?\n  user        User         @relation(fields: [userId], references: [id], onDelete: Cascade)\n  parent      Comment?     @relation(\"CommentReplies\", fields: [parentId], references: [id], onDelete: Cascade)\n  replies     Comment[]    @relation(\"CommentReplies\")\n  likes       CommentLike[]\n\n  @@index([mediaId, mediaType])\n  @@index([mediaId, mediaType, season, episode])\n  @@index([userId])\n  @@index([parentId])\n  @@index([flagged])\n  @@map(\"comments\")\n}\n\nmodel CommentLike {\n  id        Int      @id @default(autoincrement())\n  userId    Int\n  commentId Int\n  createdAt DateTime @default(now())\n  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)\n  comment   Comment  @relation(fields: [commentId], references: [id], onDelete: Cascade)\n\n  @@unique([userId, commentId])\n  @@map(\"comment_likes\")\n}\n\nmodel ImageCache {\n  id        Int      @id @default(autoincrement())\n  url       String   @unique\n  path      String\n  format    String\n  width     Int?\n  height    Int?\n  quality   Int\n  createdAt DateTime @default(now())\n  accessedAt DateTime @default(now())\n\n  @@index([url])\n  @@map(\"image_cache\")\n}\n"
  },
  {
    "path": "server.js",
    "content": "// Production server wrapper\nimport('./build/index.js').catch(err => {\n    console.error('Failed to import app:', err);\n    process.exit(1);\n});\n"
  },
  {
    "path": "src/app.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  html {\n    @apply antialiased;\n  }\n\n  body {\n    @apply bg-gray-900 text-white;\n  }\n\n  .sveltekit-body {\n    display: contents;\n  }\n\n  /* Custom scrollbar */\n  ::-webkit-scrollbar {\n    @apply w-2;\n  }\n\n  ::-webkit-scrollbar-track {\n    @apply bg-gray-800;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    @apply bg-gray-600 rounded-full hover:bg-gray-500 transition-colors;\n  }\n}\n\n@layer components {\n  .btn {\n    @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-colors;\n  }\n\n  .btn-primary {\n    @apply bg-primary-500 text-white hover:bg-primary-600;\n  }\n\n  .btn-secondary {\n    @apply bg-gray-700 text-white hover:bg-gray-600;\n  }\n\n  .input {\n    @apply px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;\n  }\n\n  .card {\n    @apply bg-gray-800 rounded-lg overflow-hidden;\n  }\n\n  .card-hover {\n    @apply transition-transform hover:scale-105;\n  }\n}\n\n/* Loading animation */\n@keyframes pulse {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.5;\n  }\n}\n\n.animate-pulse {\n  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n/* Fade animations */\n.fade-enter {\n  opacity: 0;\n}\n\n.fade-enter-active {\n  opacity: 1;\n  transition: opacity 200ms ease-in;\n}\n\n.fade-exit {\n  opacity: 1;\n}\n\n.fade-exit-active {\n  opacity: 0;\n  transition: opacity 200ms ease-out;\n}\n\n/* Hero section gradient */\n.hero-gradient {\n  background: linear-gradient(\n    to bottom,\n    transparent 0%,\n    rgba(17, 24, 39, 0.7) 50%,\n    rgba(17, 24, 39, 1) 100%\n  );\n}\n\n/* Media card hover effects */\n.media-card-overlay {\n  @apply absolute inset-0 bg-black bg-opacity-0 transition-all duration-300;\n}\n\n.media-card:hover .media-card-overlay {\n  @apply bg-opacity-60;\n}\n\n.media-card-content {\n  @apply absolute inset-0 flex flex-col justify-end p-4 opacity-0 transition-opacity duration-300;\n}\n\n.media-card:hover .media-card-content {\n  @apply opacity-100;\n}\n\n/* Toast notifications */\n.toast {\n  @apply fixed bottom-4 right-4 px-4 py-2 rounded-lg shadow-lg transform transition-all duration-300;\n}\n\n.toast-enter {\n  @apply translate-y-full opacity-0;\n}\n\n.toast-enter-active {\n  @apply translate-y-0 opacity-100;\n}\n\n.toast-exit {\n  @apply translate-y-0 opacity-100;\n}\n\n.toast-exit-active {\n  @apply translate-y-full opacity-0;\n}\n"
  },
  {
    "path": "src/app.d.ts",
    "content": "/// <reference types=\"@sveltejs/kit\" />\n\n// See https://kit.svelte.dev/docs/types#app\ndeclare global {\n  namespace App {\n    interface Error {\n      message: string;\n      code?: string;\n    }\n    interface Locals {\n      user: {\n        id: number;\n        username: string;\n        email: string;\n        isAdmin: boolean;\n      } | null;\n    }\n    interface PageData {\n      user: {\n        id: number;\n        username: string;\n        email: string;\n        isAdmin: boolean;\n      } | null;\n    }\n    interface Platform {}\n  }\n\n  namespace NodeJS {\n    interface ProcessEnv {\n      TMDB_API_KEY: string;\n      DATABASE_URL: string;\n      JWT_SECRET: string;\n    }\n  }\n}\n\nexport {};\n"
  },
  {
    "path": "src/app.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%sveltekit.assets%/favicon.png\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    %sveltekit.head%\n  </head>\n  <body data-sveltekit-preload-data=\"hover\">\n    <div class=\"sveltekit-body\">%sveltekit.body%</div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n  readonly VITE_TMDB_API_KEY: string;\n  readonly TMDB_API_KEY: string;\n  readonly JWT_SECRET: string;\n  readonly DATABASE_URL: string;\n  readonly SMTP_HOST: string;\n  readonly SMTP_PORT: string;\n  readonly SMTP_USER: string;\n  readonly SMTP_PASS: string;\n  readonly SMTP_FROM: string;\n  readonly NODE_ENV: \"development\" | \"production\";\n  readonly VIDSRC_BASE_URL: string;\n  readonly VIDLINK_BASE_URL: string;\n  readonly MOVIES111_BASE_URL: string;\n  readonly EMBED2_BASE_URL: string;\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv;\n}\n\ndeclare module \"$env/static/private\" {\n  export const JWT_SECRET: string;\n  export const DATABASE_URL: string;\n  export const TMDB_API_KEY: string;\n  export const SMTP_HOST: string;\n  export const SMTP_PORT: string;\n  export const SMTP_USER: string;\n  export const SMTP_PASS: string;\n  export const SMTP_FROM: string;\n  export const NODE_ENV: \"development\" | \"production\";\n  export const VIDSRC_BASE_URL: string;\n  export const VIDLINK_BASE_URL: string;\n  export const MOVIES111_BASE_URL: string;\n  export const EMBED2_BASE_URL: string;\n}\n\ndeclare module \"$env/static/public\" {\n  export const PUBLIC_TMDB_API_KEY: string;\n}\n"
  },
  {
    "path": "src/hooks.server.ts",
    "content": "import type { Handle } from \"@sveltejs/kit\";\nimport { getSession, createCsrfToken } from \"$lib/server/auth\";\nimport { prisma } from \"$lib/server/prisma\";\nimport { isDatabaseConnectionError } from \"$lib/server/services/db-error\";\nimport { dev } from \"$app/environment\";\nimport crypto from \"node:crypto\";\nimport { CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from \"$lib/constants/security\";\n\nconst ADMIN_RATE_LIMIT = 100;\nconst ADMIN_RATE_WINDOW = 5 * 60 * 1000;\nconst adminRateLimits = new Map<string, { count: number; firstAttempt: number }>();\nconst CSRF_MAX_AGE = 60 * 60 * 24 * 7;\n\nfunction checkAdminRateLimit(ip: string): boolean {\n  const now = Date.now();\n  const limit = adminRateLimits.get(ip);\n\n  if (!limit) {\n    adminRateLimits.set(ip, { count: 1, firstAttempt: now });\n    return true;\n  }\n\n  if (now - limit.firstAttempt >= ADMIN_RATE_WINDOW) {\n    adminRateLimits.set(ip, { count: 1, firstAttempt: now });\n    return true;\n  }\n\n  if (limit.count >= ADMIN_RATE_LIMIT) {\n    return false;\n  }\n\n  limit.count++;\n  adminRateLimits.set(ip, limit);\n  return true;\n}\n\nsetInterval(() => {\n  const now = Date.now();\n  for (const [key, limit] of adminRateLimits.entries()) {\n    if (now - limit.firstAttempt >= ADMIN_RATE_WINDOW) {\n      adminRateLimits.delete(key);\n    }\n  }\n}, ADMIN_RATE_WINDOW);\n\nfunction setSecurityHeaders(response: Response, nonce: string): void {\n  const isDev = dev;\n\n  response.headers.set('X-Frame-Options', 'DENY');\n  response.headers.set('X-Content-Type-Options', 'nosniff');\n  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n  response.headers.set(\n    'Content-Security-Policy',\n    \"default-src 'self'; \" +\n    (isDev\n      ? \"script-src 'self' 'unsafe-inline' 'unsafe-eval'; \"\n      : `script-src 'self' 'nonce-${nonce}'; `) +\n    (isDev\n      ? \"style-src 'self' 'unsafe-inline'; \"\n      : `style-src 'self' 'nonce-${nonce}'; `) +\n    \"img-src 'self' data: https:; \" +\n    \"font-src 'self' data:; \" +\n    \"frame-src 'self' \" +\n    \"https://vidsrc.cc/ https://*.vidsrc.cc/ \" +\n    \"https://vidplay.site/ https://*.vidplay.site/ \" +\n    \"https://vidplay.online/ https://*.vidplay.online/ \" +\n    \"https://vidlink.pro/ https://*.vidlink.pro/ \" +\n    \"https://111movies.com/ https://*.111movies.com/ \" +\n    \"https://2embed.cc/ https://*.2embed.cc/;\"\n  );\n}\n\nexport const handle: Handle = async ({ event, resolve }) => {\n  const nonce = crypto.randomBytes(16).toString(\"base64\");\n  const isAdminRoute = event.url.pathname.startsWith('/admin');\n  const method = event.request.method.toUpperCase();\n  const hasSessionCookie = Boolean(event.cookies.get(\"session\"));\n\n  if (![\"GET\", \"HEAD\", \"OPTIONS\"].includes(method) && hasSessionCookie) {\n    const origin = event.request.headers.get(\"origin\");\n    if (origin && origin !== event.url.origin) {\n      const response = new Response(\"Cross-origin requests not allowed\", { status: 403 });\n      setSecurityHeaders(response, nonce);\n      return response;\n    }\n\n    const csrfHeader = event.request.headers.get(CSRF_HEADER_NAME);\n    const csrfCookie = event.cookies.get(CSRF_COOKIE_NAME);\n    if (!csrfHeader || !csrfCookie || csrfHeader !== csrfCookie) {\n      const response = new Response(\"Invalid CSRF token\", { status: 403 });\n      setSecurityHeaders(response, nonce);\n      return response;\n    }\n  }\n\n  // Authenticate user BEFORE resolving the request\n  try {\n    const session = await getSession(event.cookies);\n\n    if (session?.userId) {\n      const user = await prisma.user.findUnique({\n        where: {\n          id: session.userId,\n        },\n        select: {\n          id: true,\n          username: true,\n          email: true,\n          isAdmin: true,\n        },\n      });\n\n      if (user) {\n        event.locals.user = {\n          id: user.id,\n          username: user.username,\n          email: user.email || '',\n          isAdmin: user.isAdmin,\n        };\n\n        if (isAdminRoute) {\n          if (!user.isAdmin) {\n            const response = new Response('Unauthorized', { status: 403 });\n            setSecurityHeaders(response, nonce);\n            return response;\n          }\n\n          const clientIp = event.getClientAddress();\n          if (!checkAdminRateLimit(clientIp)) {\n            const response = new Response('Too Many Requests', { status: 429 });\n            setSecurityHeaders(response, nonce);\n            return response;\n          }\n        }\n        if (!event.cookies.get(CSRF_COOKIE_NAME)) {\n          const csrfToken = createCsrfToken();\n          event.cookies.set(CSRF_COOKIE_NAME, csrfToken, {\n            path: \"/\",\n            sameSite: \"strict\",\n            secure: !dev,\n            httpOnly: false,\n            maxAge: CSRF_MAX_AGE,\n          });\n        }\n      } else {\n        event.cookies.delete(\"session\", { path: \"/\" });\n\n        if (isAdminRoute) {\n          const response = new Response('Unauthorized', { status: 403 });\n          setSecurityHeaders(response, nonce);\n          return response;\n        }\n      }\n    } else if (isAdminRoute) {\n      const response = new Response('Unauthorized', { status: 403 });\n      setSecurityHeaders(response, nonce);\n      return response;\n    }\n  } catch (error) {\n    // Handle database connection errors gracefully\n    if (isDatabaseConnectionError(error)) {\n      console.error(\"Database unavailable during session validation\");\n      // Don't delete session cookie - DB might come back\n      // For admin routes, return 503; for others, continue without auth\n      if (isAdminRoute) {\n        const response = new Response('Service temporarily unavailable', { status: 503 });\n        setSecurityHeaders(response, nonce);\n        return response;\n      }\n      // Continue without user context for non-admin routes\n    } else {\n      event.cookies.delete(\"session\", { path: \"/\" });\n      console.error(\"Session validation error:\", error);\n\n      if (isAdminRoute) {\n        const response = new Response('Unauthorized', { status: 403 });\n        setSecurityHeaders(response, nonce);\n        return response;\n      }\n    }\n  }\n\n  // Only resolve AFTER auth checks pass\n  const response = await resolve(event, {\n    transformPageChunk: ({ html }) =>\n      html\n        .replace(/<script(?![^>]*nonce=)/g, `<script nonce=\\\"${nonce}\\\"`)\n        .replace(/<style(?![^>]*nonce=)/g, `<style nonce=\\\"${nonce}\\\"`),\n  });\n  setSecurityHeaders(response, nonce);\n\n  return response;\n};\n"
  },
  {
    "path": "src/lib/components/Captcha.svelte",
    "content": "<script lang=\"ts\">\n  import { createEventDispatcher } from 'svelte';\n  import { onMount } from 'svelte';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  export let required = true;\n\n  const dispatch = createEventDispatcher<{ verify: { valid: boolean; captchaId: string; answer: string } }>();\n\n  let captchaId = '';\n  let captchaText = '';\n  let userInput = '';\n  let canvas: HTMLCanvasElement;\n  let loading = false;\n  let verified = false;\n\n  async function fetchCaptcha() {\n    loading = true;\n    verified = false;\n    userInput = '';\n\n    try {\n      const response = await fetch('/api/captcha');\n      if (!response.ok) {\n        throw new Error('Failed to fetch captcha');\n      }\n      const data = await response.json();\n      captchaId = data.id;\n      captchaText = data.text;\n      renderCaptcha();\n    } catch (error) {\n      console.error('Error fetching captcha:', error);\n    } finally {\n      loading = false;\n    }\n  }\n\n  function renderCaptcha() {\n    const ctx = canvas?.getContext('2d');\n    if (!ctx || !captchaText) return;\n\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n    const gradient = ctx.createRadialGradient(\n      canvas.width/2, canvas.height/2, 0,\n      canvas.width/2, canvas.height/2, canvas.width/2\n    );\n    gradient.addColorStop(0, '#1a1e2d');\n    gradient.addColorStop(0.5, '#1f2937');\n    gradient.addColorStop(1, '#1a1e2d');\n    ctx.fillStyle = gradient;\n    ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n    for (let i = 0; i < 3; i++) {\n      ctx.beginPath();\n      const startY = Math.random() * canvas.height;\n      const endY = Math.random() * canvas.height;\n      const controlY = Math.random() * canvas.height;\n\n      ctx.moveTo(0, startY);\n      ctx.quadraticCurveTo(\n        canvas.width/2, controlY,\n        canvas.width, endY\n      );\n\n      ctx.strokeStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.1)`;\n      ctx.lineWidth = 15;\n      ctx.stroke();\n    }\n\n    for (let i = 0; i < 150; i++) {\n      ctx.beginPath();\n      ctx.arc(\n        Math.random() * canvas.width,\n        Math.random() * canvas.height,\n        Math.random() * 2,\n        0,\n        Math.PI * 2\n      );\n      ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.3)`;\n      ctx.fill();\n    }\n\n    const chars_array = captchaText.split('');\n    const char_width = canvas.width / (chars_array.length + 1);\n\n    chars_array.forEach((char, i) => {\n      ctx.save();\n      const x = char_width * (i + 0.8) + (Math.random() - 0.5) * 15;\n      const y = canvas.height / 2 + (Math.random() - 0.5) * 20;\n\n      ctx.translate(x, y);\n      ctx.rotate((Math.random() - 0.5) * 0.8);\n\n      const fonts = ['Arial Black', 'Impact', 'Verdana', 'Times New Roman'];\n      const randomFont = fonts[Math.floor(Math.random() * fonts.length)];\n      const fontSize = Math.floor(Math.random() * 10) + 28;\n      ctx.font = `bold ${fontSize}px ${randomFont}`;\n\n      for (let j = 0; j < 2; j++) {\n        ctx.shadowColor = `rgba(0, 0, 0, ${0.2 + Math.random() * 0.3})`;\n        ctx.shadowBlur = 4 + Math.random() * 4;\n        ctx.shadowOffsetX = (Math.random() - 0.5) * 6;\n        ctx.shadowOffsetY = (Math.random() - 0.5) * 6;\n        ctx.fillStyle = `rgba(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100}, 0.3)`;\n        ctx.fillText(char, 0, 0);\n      }\n\n      ctx.restore();\n    });\n\n    chars_array.forEach((char, i) => {\n      ctx.save();\n      const x = char_width * (i + 0.8) + (Math.random() - 0.5) * 15;\n      const y = canvas.height / 2 + (Math.random() - 0.5) * 20;\n\n      ctx.translate(x, y);\n      ctx.rotate((Math.random() - 0.5) * 0.8);\n\n      const fonts = ['Arial Black', 'Impact', 'Verdana', 'Times New Roman'];\n      const randomFont = fonts[Math.floor(Math.random() * fonts.length)];\n      const fontSize = Math.floor(Math.random() * 10) + 28;\n      ctx.font = `bold ${fontSize}px ${randomFont}`;\n\n      const brightness = Math.random() * 100 + 120;\n      ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`;\n\n      ctx.transform(1, Math.random() * 0.3 - 0.15, Math.random() * 0.3 - 0.15, 1, 0, 0);\n      ctx.fillText(char, 0, 0);\n\n      if (Math.random() > 0.5) {\n        ctx.beginPath();\n        ctx.moveTo(-fontSize/2, -fontSize/2);\n        ctx.lineTo(fontSize/2, fontSize/2);\n        ctx.strokeStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.5)`;\n        ctx.lineWidth = 1 + Math.random();\n        ctx.stroke();\n      }\n\n      ctx.restore();\n    });\n\n    for (let i = 0; i < 8; i++) {\n      ctx.beginPath();\n      const startX = Math.random() * canvas.width;\n      const startY = 0;\n      const endX = Math.random() * canvas.width;\n      const endY = canvas.height;\n\n      ctx.moveTo(startX, startY);\n      ctx.lineTo(endX, endY);\n      ctx.strokeStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.3)`;\n      ctx.lineWidth = 1 + Math.random();\n      ctx.stroke();\n    }\n\n    for (let i = 0; i < 3; i++) {\n      ctx.beginPath();\n      let x = 0;\n      let y = Math.random() * canvas.height;\n      ctx.moveTo(x, y);\n\n      while (x < canvas.width) {\n        x += 10;\n        y = y + Math.sin(x * 0.05) * 15;\n        ctx.lineTo(x, y);\n      }\n\n      ctx.strokeStyle = `rgba(255, 255, 255, 0.1)`;\n      ctx.lineWidth = 2;\n      ctx.stroke();\n    }\n  }\n\n  async function verifyCaptcha() {\n    if (!userInput || !captchaId) {\n      dispatch('verify', { valid: false, captchaId: '', answer: '' });\n      return;\n    }\n\n    // Dispatch with data for server-side validation during form submission\n    // We also do a preliminary server check here\n    try {\n      const response = await csrfFetch('/api/captcha', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ id: captchaId, answer: userInput }),\n      });\n\n      const result = await response.json();\n      verified = result.valid;\n\n      dispatch('verify', {\n        valid: result.valid,\n        captchaId,\n        answer: userInput\n      });\n\n      if (!result.valid) {\n        // Get a new captcha on failure\n        await fetchCaptcha();\n      }\n    } catch {\n      dispatch('verify', { valid: false, captchaId: '', answer: '' });\n      await fetchCaptcha();\n    }\n  }\n\n  onMount(fetchCaptcha);\n</script>\n\n<div class=\"flex flex-col gap-4\">\n  <div class=\"flex items-center gap-4\">\n    <canvas\n      bind:this={canvas}\n      width=\"200\"\n      height=\"60\"\n      class=\"border border-gray-600 rounded bg-gray-700\"\n      aria-label=\"CAPTCHA image\"\n    ></canvas>\n    <button\n      type=\"button\"\n      class=\"p-2 rounded hover:bg-gray-700 text-gray-300 disabled:opacity-50\"\n      on:click={fetchCaptcha}\n      disabled={loading}\n      aria-label=\"Generate new CAPTCHA\"\n    >\n      {#if loading}\n        <svg class=\"w-5 h-5 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n          <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n          <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n        </svg>\n      {:else}\n        <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n        </svg>\n      {/if}\n    </button>\n    {#if verified}\n      <svg class=\"w-6 h-6 text-green-500\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n      </svg>\n    {/if}\n  </div>\n\n  <div class=\"flex flex-col gap-2\">\n    <label for=\"captcha-input\" class=\"text-sm font-medium text-gray-300\">\n      Enter the text shown above\n      {#if required}\n        <span class=\"text-red-500\">*</span>\n      {/if}\n    </label>\n    <input\n      id=\"captcha-input\"\n      type=\"text\"\n      bind:value={userInput}\n      on:blur={verifyCaptcha}\n      class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n      class:border-green-500={verified}\n      {required}\n      disabled={loading}\n      aria-required={required}\n      aria-label=\"CAPTCHA verification input\"\n    />\n  </div>\n</div>\n"
  },
  {
    "path": "src/lib/components/CommentForm.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import RichTextEditor from './RichTextEditor.svelte';\n  import { authStore } from '$lib/stores/auth';\n  import { validateComment } from '$lib/shared/comment-validation';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  export let mediaId: number;\n  export let mediaType: 'movie' | 'tv';\n  export let season: number | undefined = undefined;\n  export let episode: number | undefined = undefined;\n  export let onCommentAdded: (comment: {\n    id: number;\n    content: string;\n    createdAt: string;\n    user: {\n      id: number;\n      username: string;\n    };\n    replies: never[];\n    _count: { likes: number };\n    isLiked: boolean;\n    flagged: boolean;\n    parentId: null;\n  }) => void;\n\n  let content = '<p></p>';\n  let isSubmitting = false;\n  let error = '';\n  let charCount = 0;\n  let isValid = false;\n  let editor: RichTextEditor;\n  const MAX_CHARS = 1000;\n\n  $: {\n    if (content === '<p></p>') {\n      isValid = false;\n      error = '';\n      charCount = 0;\n    } else {\n      const validation = validateComment(content);\n      isValid = validation.isValid && charCount <= MAX_CHARS;\n      if (!validation.isValid && validation.error) {\n        error = validation.error;\n      } else if (charCount > MAX_CHARS) {\n        error = 'Comment is too long';\n      } else {\n        error = '';\n      }\n    }\n  }\n\n  async function handleSubmit() {\n    if (!isValid || isSubmitting) return;\n\n    try {\n      isSubmitting = true;\n      error = '';\n\n      const validation = validateComment(content);\n      if (!validation.isValid) {\n        error = validation.error || 'Invalid comment';\n        return;\n      }\n\n      const response = await csrfFetch('/api/comments', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          mediaId,\n          mediaType,\n          content,\n          season: mediaType === 'tv' ? season : undefined,\n          episode: mediaType === 'tv' ? episode : undefined\n        })\n      });\n\n      if (!response.ok) {\n        const data = await response.json();\n        throw new Error(data.error || 'Failed to post comment');\n      }\n\n      const newComment = await response.json();\n      editor?.clear();\n      onCommentAdded({\n        ...newComment,\n        user: $authStore.user!,\n        replies: [],\n        _count: { likes: 0 },\n        isLiked: false,\n        flagged: false\n      });\n    } catch (err) {\n      if (err instanceof Error) {\n        error = err.message;\n      } else {\n        error = 'An unexpected error occurred';\n      }\n    } finally {\n      isSubmitting = false;\n    }\n  }\n\n  function handleContentInput(event: CustomEvent<string>) {\n    content = event.detail;\n\n    const textContent = content.replace(/<[^>]*>/g, '');\n    charCount = textContent.length;\n  }\n</script>\n\n<div class=\"bg-gray-800/50 rounded-lg p-6 backdrop-blur-sm border border-gray-700/50\">\n  <h3 class=\"text-xl font-semibold mb-4\">Add a Comment</h3>\n\n  {#if $authStore.isAuthenticated}\n    <form on:submit|preventDefault={handleSubmit} class=\"space-y-4\">\n      <div class=\"space-y-2\">\n        <RichTextEditor\n          bind:this={editor}\n          bind:content\n          on:input={handleContentInput}\n          class_=\"min-h-[120px] bg-gray-900/50\"\n        />\n\n        <div class=\"flex justify-between items-center text-sm\">\n          <span class=\"text-gray-400\">\n            {charCount}/{MAX_CHARS} characters\n          </span>\n          {#if error}\n            <span class=\"text-red-400\">{error}</span>\n          {/if}\n        </div>\n      </div>\n\n      <div class=\"flex justify-end\">\n        <button\n          type=\"submit\"\n          class=\"px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n          disabled={!isValid || isSubmitting}\n        >\n          {isSubmitting ? 'Posting...' : 'Post Comment'}\n        </button>\n      </div>\n    </form>\n  {:else}\n    <div class=\"text-center py-6\">\n      <p class=\"text-gray-400\">Please <a href=\"/login\" class=\"text-blue-400 hover:underline\">log in</a> to leave a comment.</p>\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/CommentList.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import { browser } from '$app/environment';\n  import { authStore } from '$lib/stores/auth';\n  import { formatDistanceToNow } from 'date-fns';\n  import DOMPurify from 'isomorphic-dompurify';\n  import ReplyForm from './ReplyForm.svelte';\n  import CommentForm from './CommentForm.svelte';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  export let mediaId: number;\n  export let mediaType: 'movie' | 'tv';\n  export let season: number | undefined = undefined;\n  export let episode: number | undefined = undefined;\n\n  interface User {\n    id: number;\n    username: string;\n  }\n\n  interface CommentLike {\n    id: number;\n    userId: number;\n    commentId: number;\n  }\n\n  interface Comment {\n    id: number;\n    content: string;\n    createdAt: string;\n    user: User;\n    replies: Comment[];\n    _count: {\n      likes: number;\n    };\n    isLiked: boolean;\n    flagged: boolean;\n    parentId: number | null;\n  }\n\n  type SortOption = 'recent' | 'likes';\n  let sortBy: SortOption = 'recent';\n  let comments: Comment[] = [];\n  let isLoading = true;\n  let error = '';\n  let page = 1;\n  let totalPages = 1;\n  let replyingToId: number | null = null;\n  let mounted = false;\n\n  $: sortedComments = [...comments].sort((a, b) => {\n    if (sortBy === 'likes') {\n      return b._count.likes - a._count.likes;\n    }\n    return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();\n  });\n\n  function formatDate(date: string) {\n    try {\n      return formatDistanceToNow(new Date(date), { addSuffix: true });\n    } catch (e) {\n      return 'just now';\n    }\n  }\n\n  function sanitizeContent(content: string): string {\n    return DOMPurify.sanitize(content, {\n      ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'br'],\n      ALLOWED_ATTR: []\n    });\n  }\n\n  async function loadComments() {\n    if (!browser || !mounted) return;\n\n    try {\n      isLoading = true;\n      error = '';\n      let url = `/api/comments?mediaId=${mediaId}&mediaType=${mediaType}&page=${page}`;\n      if (mediaType === 'tv' && season !== undefined && episode !== undefined) {\n        url += `&season=${season}&episode=${episode}`;\n      }\n      const response = await fetch(url);\n\n      if (!response.ok) {\n        const errorData = await response.json().catch(() => null);\n        throw new Error(errorData?.message || `Error ${response.status}: ${response.statusText}`);\n      }\n\n      const data = await response.json();\n\n      if (!data || !Array.isArray(data.comments)) {\n        throw new Error('Invalid response format from server');\n      }\n\n      comments = data.comments;\n      totalPages = data.totalPages;\n    } catch (e) {\n      console.error('Error loading comments:', e);\n      error = e instanceof Error ? e.message : 'Failed to load comments';\n    } finally {\n      isLoading = false;\n    }\n  }\n\n  async function handleLike(commentId: number) {\n    if (!$authStore.isAuthenticated) return;\n\n    let comment = comments.find(c => c.id === commentId);\n    if (!comment) {\n      for (const topComment of comments) {\n        comment = topComment.replies.find(r => r.id === commentId);\n        if (comment) break;\n      }\n    }\n    if (!comment) return;\n\n    comment.isLiked = !comment.isLiked;\n    comment._count.likes += comment.isLiked ? 1 : -1;\n    comments = [...comments];\n\n    try {\n      const response = await csrfFetch('/api/comments/like', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ commentId })\n      });\n\n      if (!response.ok) {\n        comment.isLiked = !comment.isLiked;\n        comment._count.likes += comment.isLiked ? 1 : -1;\n        comments = [...comments];\n\n        const errorData = await response.json().catch(() => null);\n        throw new Error(errorData?.message || 'Failed to like comment');\n      }\n    } catch (e) {\n      console.error('Error liking comment:', e);\n    }\n  }\n\n  async function handleFlag(commentId: number) {\n    if (!$authStore.isAuthenticated) return;\n\n    const reason = prompt('Please provide a reason for reporting this comment:');\n    if (reason === null) return;\n\n    let comment = comments.find(c => c.id === commentId);\n    if (!comment) {\n      for (const topComment of comments) {\n        comment = topComment.replies.find(r => r.id === commentId);\n        if (comment) break;\n      }\n    }\n    if (!comment) return;\n\n    comment.flagged = true;\n    comments = [...comments];\n\n    try {\n      const response = await csrfFetch(`/api/comments/${commentId}/flag`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({ reason })\n      });\n\n      if (!response.ok) {\n        comment.flagged = false;\n        comments = [...comments];\n\n        const errorData = await response.json().catch(() => null);\n        throw new Error(errorData?.message || 'Failed to flag comment');\n      }\n    } catch (e) {\n      console.error('Error flagging comment:', e);\n    }\n  }\n\n  function handleReplyClick(commentId: number) {\n    if (!$authStore.isAuthenticated) return;\n    replyingToId = commentId;\n  }\n\n  function handleReplyCancel() {\n    replyingToId = null;\n  }\n\n  function handleReplyAdded(newReply: Comment) {\n    const parentComment = comments.find(c => c.id === newReply.parentId);\n    if (parentComment) {\n      parentComment.replies = [...parentComment.replies, newReply];\n      comments = [...comments];\n    }\n    replyingToId = null;\n  }\n\n  function handleCommentAdded(newComment: Comment) {\n    comments = [newComment, ...comments];\n  }\n\n  $: if (mounted && mediaType === 'tv' && browser) {\n    const episodeKey = `${season}-${episode}`;\n    loadComments();\n  }\n\n  onMount(() => {\n    mounted = true;\n    loadComments();\n  });\n</script>\n\n<div class=\"space-y-6\">\n  <CommentForm\n    {mediaId}\n    {mediaType}\n    {season}\n    {episode}\n    onCommentAdded={handleCommentAdded}\n  />\n\n  <div class=\"flex items-center gap-4 text-sm text-gray-400\">\n    <span>Sort by:</span>\n    <button\n      class=\"hover:text-white transition-colors\"\n      class:text-white={sortBy === 'recent'}\n      on:click={() => sortBy = 'recent'}\n    >\n      Most Recent\n    </button>\n    <span>•</span>\n    <button\n      class=\"hover:text-white transition-colors\"\n      class:text-white={sortBy === 'likes'}\n      on:click={() => sortBy = 'likes'}\n    >\n      Most Liked\n    </button>\n  </div>\n\n  {#if isLoading}\n    <div class=\"flex justify-center py-8\">\n      <div class=\"animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500\"></div>\n    </div>\n  {:else if error}\n    <div class=\"bg-red-500/10 border border-red-500/20 rounded-lg p-6 text-center\">\n      <p class=\"text-red-400\">{error}</p>\n      <button\n        class=\"mt-4 px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors\"\n        on:click={loadComments}\n      >\n        Try Again\n      </button>\n    </div>\n  {:else if comments.length === 0}\n    <div class=\"bg-gray-800/50 rounded-lg p-8 text-center backdrop-blur-sm border border-gray-700/50\">\n      <div class=\"flex flex-col items-center gap-4\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 text-gray-500\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z\" />\n        </svg>\n        <div>\n          <p class=\"text-gray-400 text-lg mb-2\">No comments yet</p>\n          <p class=\"text-gray-500\">Be the first to share your thoughts!</p>\n        </div>\n      </div>\n    </div>\n  {:else}\n    <div class=\"space-y-4\">\n      {#each sortedComments as comment (comment.id)}\n        {#if !comment.parentId}\n          <div class=\"bg-gray-800/50 rounded-lg p-6 backdrop-blur-sm border border-gray-700/50\">\n            <div class=\"flex justify-between items-start mb-3\">\n              <div class=\"flex items-center gap-3\">\n                <div class=\"w-10 h-10 bg-gray-700 rounded-full flex items-center justify-center\">\n                  <span class=\"text-lg font-semibold\">{comment.user.username[0].toUpperCase()}</span>\n                </div>\n                <div>\n                  <div class=\"font-semibold\">{comment.user.username}</div>\n                  <div class=\"text-sm text-gray-400\">\n                    {formatDate(comment.createdAt)}\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            <div class=\"prose prose-invert max-w-none\">\n              {@html sanitizeContent(comment.content)}\n            </div>\n\n            <div class=\"flex items-center gap-4 mt-4 pt-4 border-t border-gray-700/50\">\n              <button\n                class=\"flex items-center gap-2 text-sm text-gray-400 hover:text-blue-400 transition-colors\"\n                class:text-blue-400={comment.isLiked}\n                on:click={() => handleLike(comment.id)}\n                disabled={!$authStore.isAuthenticated}\n              >\n                <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5\" />\n                </svg>\n                <span>{comment._count.likes}</span>\n              </button>\n\n              {#if $authStore.isAuthenticated}\n                <button\n                  class=\"flex items-center gap-2 text-sm text-gray-400 hover:text-blue-400 transition-colors\"\n                  on:click={() => handleReplyClick(comment.id)}\n                >\n                  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6\" />\n                  </svg>\n                  <span>Reply</span>\n                </button>\n              {/if}\n\n              {#if $authStore.isAuthenticated && !comment.flagged}\n                <button\n                  class=\"flex items-center gap-2 text-sm text-gray-400 hover:text-red-400 transition-colors\"\n                  on:click={() => handleFlag(comment.id)}\n                >\n                  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9\" />\n                  </svg>\n                  <span>Report</span>\n                </button>\n              {/if}\n            </div>\n\n            {#if replyingToId === comment.id}\n              <div class=\"mt-4 pl-8\">\n                <ReplyForm\n                  {mediaId}\n                  {mediaType}\n                  {season}\n                  {episode}\n                  parentId={comment.id}\n                  onReplyAdded={handleReplyAdded}\n                  onCancel={handleReplyCancel}\n                />\n              </div>\n            {/if}\n\n            {#if comment.replies.length > 0}\n              <div class=\"mt-4 pl-8 space-y-4\">\n                {#each comment.replies as reply (reply.id)}\n                  <div class=\"bg-gray-800/30 rounded-lg p-4\">\n                    <div class=\"flex items-center gap-3 mb-2\">\n                      <div class=\"w-8 h-8 bg-gray-700 rounded-full flex items-center justify-center\">\n                        <span class=\"text-sm font-semibold\">{reply.user.username[0].toUpperCase()}</span>\n                      </div>\n                      <div>\n                        <div class=\"font-semibold\">{reply.user.username}</div>\n                        <div class=\"text-xs text-gray-400\">\n                          {formatDate(reply.createdAt)}\n                        </div>\n                      </div>\n                    </div>\n                    <div class=\"prose prose-invert max-w-none\">\n                      {@html sanitizeContent(reply.content)}\n                    </div>\n                    <div class=\"flex items-center gap-4 mt-4 pt-4 border-t border-gray-700/30\">\n                      <button\n                        class=\"flex items-center gap-2 text-xs text-gray-400 hover:text-blue-400 transition-colors\"\n                        class:text-blue-400={reply.isLiked}\n                        on:click={() => handleLike(reply.id)}\n                        disabled={!$authStore.isAuthenticated}\n                      >\n                        <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5\" />\n                        </svg>\n                        <span>{reply._count.likes}</span>\n                      </button>\n\n                      {#if $authStore.isAuthenticated && !reply.flagged}\n                        <button\n                          class=\"flex items-center gap-2 text-xs text-gray-400 hover:text-red-400 transition-colors\"\n                          on:click={() => handleFlag(reply.id)}\n                        >\n                          <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9\" />\n                          </svg>\n                          <span>Report</span>\n                        </button>\n                      {/if}\n                    </div>\n                  </div>\n                {/each}\n              </div>\n            {/if}\n          </div>\n        {/if}\n      {/each}\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/CommentModeration.svelte",
    "content": "<script lang=\"ts\">\n  import { page } from '$app/stores';\n  import { commentsStore } from '$lib/stores/comments';\n  import type { Comment } from '@prisma/client';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  export let comment: Comment;\n\n  const isAdmin = $page.data.user?.isAdmin ?? false;\n\n  async function deleteComment() {\n    try {\n      const response = await csrfFetch(`/api/comments/${comment.id}`, {\n        method: 'DELETE',\n        headers: {\n          'Content-Type': 'application/json'\n        }\n      });\n\n      if (!response.ok) {\n        throw new Error('Failed to delete comment');\n      }\n\n      commentsStore.deleteComment(comment.id);\n    } catch (error) {\n      console.error('Error deleting comment:', error);\n    }\n  }\n\n  async function flagComment() {\n    try {\n      const response = await csrfFetch(`/api/comments/${comment.id}/flag`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        }\n      });\n\n      if (!response.ok) {\n        throw new Error('Failed to flag comment');\n      }\n\n      commentsStore.updateComment(comment.id, comment.content);\n    } catch (error) {\n      console.error('Error flagging comment:', error);\n    }\n  }\n\n  async function unflagComment() {\n    try {\n      const response = await csrfFetch(`/api/comments/${comment.id}/unflag`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        }\n      });\n\n      if (!response.ok) {\n        throw new Error('Failed to unflag comment');\n      }\n\n      commentsStore.updateComment(comment.id, comment.content);\n    } catch (error) {\n      console.error('Error unflagging comment:', error);\n    }\n  }\n</script>\n\n{#if isAdmin}\n  <div class=\"flex items-center gap-2\">\n    <button\n      class=\"text-sm text-red-500 hover:text-red-600\"\n      on:click={deleteComment}\n      title=\"Delete comment\"\n    >\n      <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n      </svg>\n    </button>\n\n    {#if comment.flagged}\n      <button\n        class=\"text-sm text-green-500 hover:text-green-600\"\n        on:click={unflagComment}\n        title=\"Unflag comment\"\n      >\n        <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n        </svg>\n      </button>\n    {:else}\n      <button\n        class=\"text-sm text-yellow-500 hover:text-yellow-600\"\n        on:click={flagComment}\n        title=\"Flag comment\"\n      >\n        <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9\" />\n        </svg>\n      </button>\n    {/if}\n  </div>\n{/if}\n"
  },
  {
    "path": "src/lib/components/EmojiPicker.svelte",
    "content": "<script lang=\"ts\">\n  import { createEventDispatcher, onDestroy } from 'svelte';\n  import data from '@emoji-mart/data';\n  import * as emojiMart from 'emoji-mart';\n\n  export let buttonClass = '';\n  export let disabled = false;\n\n  const dispatch = createEventDispatcher<{ select: string }>();\n  let showPicker = false;\n  let pickerElement: HTMLDivElement;\n  let buttonElement: HTMLButtonElement;\n  type EmojiData = { native: string };\n  type EmojiMartPicker = { destroy?: () => void };\n  let picker: EmojiMartPicker | null = null;\n\n  emojiMart.init({ data });\n\n  function handleClickOutside(event: MouseEvent) {\n    if (showPicker &&\n        pickerElement &&\n        !pickerElement.contains(event.target as Node) &&\n        buttonElement &&\n        !buttonElement.contains(event.target as Node)) {\n      showPicker = false;\n    }\n  }\n\n  function togglePicker() {\n    if (!disabled) {\n      showPicker = !showPicker;\n    }\n  }\n\n  function handleSelect(emoji: EmojiData) {\n    dispatch('select', emoji.native);\n    showPicker = false;\n  }\n\n  function createPicker() {\n    if (showPicker && pickerElement) {\n      if (picker) {\n        picker.destroy?.();\n      }\n      return new emojiMart.Picker({\n        parent: pickerElement,\n        data,\n        onEmojiSelect: handleSelect,\n        theme: 'dark',\n        showPreview: false,\n        showSkinTones: false,\n        emojiSize: 20,\n        emojiButtonSize: 28,\n        maxFrequentRows: 0,\n      });\n    }\n    return null;\n  }\n\n  $: picker = createPicker();\n\n  onDestroy(() => {\n    if (picker) {\n      picker.destroy?.();\n    }\n  });\n</script>\n\n<svelte:window on:click={handleClickOutside} />\n\n<div class=\"relative\">\n  <button\n    bind:this={buttonElement}\n    type=\"button\"\n    class=\"p-1 rounded hover:bg-gray-700/50 transition-colors {buttonClass}\"\n    class:opacity-50={disabled}\n    class:cursor-not-allowed={disabled}\n    on:click={togglePicker}\n    {disabled}\n    aria-label=\"Add emoji\"\n    title=\"Add emoji\"\n  >\n    <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n      <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n    </svg>\n  </button>\n\n  {#if showPicker}\n    <div\n      bind:this={pickerElement}\n      class=\"absolute bottom-full right-0 mb-2 z-50\"\n    ></div>\n  {/if}\n</div>\n\n<style>\n  :global(em-emoji-picker) {\n    --rgb-background: 31 41 55;\n    --rgb-input: 55 65 81;\n    --rgb-color: 209 213 219;\n    height: 350px !important;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/components/EpisodeSelector.svelte",
    "content": "<script lang=\"ts\">\n  import { createEventDispatcher } from 'svelte';\n\n  interface Season {\n    season_number: number;\n    name: string;\n    episode_count: number;\n  }\n\n  interface Episode {\n    episode_number: number;\n    name: string;\n    overview: string;\n  }\n\n  export let mediaId: number;\n  export let showModal = false;\n\n  const dispatch = createEventDispatcher();\n  let seasons: Season[] = [];\n  let episodes: Episode[] = [];\n  let selectedSeason: number | undefined;\n  let selectedEpisode: number | undefined;\n\n  $: if (showModal && mediaId && !seasons.length) {\n    loadSeasons();\n  }\n\n  async function loadSeasons() {\n    try {\n      const response = await fetch(`/api/tv/${mediaId}/seasons`);\n      if (response.ok) {\n        const data = await response.json();\n        seasons = data.seasons.filter((s: Season) => s.season_number > 0);\n        if (seasons.length > 0) {\n          await selectSeason(seasons[0].season_number);\n        }\n      }\n    } catch (error) {\n      console.error('Error fetching seasons:', error);\n    }\n  }\n\n  async function selectSeason(seasonNumber: number) {\n    selectedSeason = seasonNumber;\n    try {\n      const response = await fetch(`/api/tv/${mediaId}/season/${seasonNumber}`);\n      if (response.ok) {\n        const data = await response.json();\n        episodes = data.episodes;\n      }\n    } catch (error) {\n      console.error('Error fetching episodes:', error);\n    }\n  }\n\n  function selectEpisode(episodeNumber: number) {\n    selectedEpisode = episodeNumber;\n    dispatch('select', { season: selectedSeason, episode: episodeNumber });\n    closeModal();\n  }\n\n  function closeModal() {\n    showModal = false;\n    dispatch('close');\n  }\n</script>\n\n{#if showModal}\n  <div class=\"fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4\">\n    <div class=\"bg-gray-800 rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col\">\n      <!-- Header -->\n      <div class=\"flex justify-between items-center p-4 border-b border-gray-700\">\n        <h2 class=\"text-xl font-semibold\">Select Episode</h2>\n        <button\n          type=\"button\"\n          class=\"text-gray-400 hover:text-white\"\n          on:click={closeModal}\n          aria-label=\"Close episode selection\"\n        >\n          <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n          </svg>\n        </button>\n      </div>\n\n      <!-- Content -->\n      <div class=\"flex-1 overflow-hidden flex divide-x divide-gray-700\">\n        <!-- Seasons -->\n        <div class=\"w-1/3 overflow-y-auto\">\n          <div class=\"p-2\">\n            {#each seasons as season}\n              <button\n                type=\"button\"\n                class=\"w-full px-3 py-2 rounded text-left text-sm transition-colors mb-1\"\n                class:bg-primary-500={selectedSeason === season.season_number}\n                class:bg-gray-700={selectedSeason !== season.season_number}\n                class:hover:bg-primary-600={selectedSeason === season.season_number}\n                class:hover:bg-gray-600={selectedSeason !== season.season_number}\n                on:click={() => selectSeason(season.season_number)}\n              >\n                Season {season.season_number}\n              </button>\n            {/each}\n          </div>\n        </div>\n\n        <!-- Episodes -->\n        <div class=\"w-2/3 overflow-y-auto\">\n          <div class=\"p-2\">\n            {#if selectedSeason && episodes.length > 0}\n              {#each episodes as episode}\n                <button\n                  type=\"button\"\n                  class=\"w-full px-3 py-2 rounded text-left text-sm transition-colors mb-1\"\n                  class:bg-primary-500={selectedEpisode === episode.episode_number}\n                  class:bg-gray-700={selectedEpisode !== episode.episode_number}\n                  class:hover:bg-primary-600={selectedEpisode === episode.episode_number}\n                  class:hover:bg-gray-600={selectedEpisode !== episode.episode_number}\n                  on:click={() => selectEpisode(episode.episode_number)}\n                >\n                  <div class=\"font-medium\">\n                    Episode {episode.episode_number}\n                  </div>\n                  <div class=\"text-xs text-gray-400 mt-0.5 line-clamp-1\">\n                    {episode.name}\n                  </div>\n                </button>\n              {/each}\n            {:else}\n              <div class=\"text-gray-400 text-sm p-3\">\n                Select a season to view episodes\n              </div>\n            {/if}\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n{/if}\n"
  },
  {
    "path": "src/lib/components/Hero.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import type { TMDBMediaResponse } from '$lib/types/tmdb';\n\n  export let media: TMDBMediaResponse;\n  export let type: 'movie' | 'tv';\n\n  let backdropUrl = '';\n\n  $: if (media?.backdrop_path) {\n    backdropUrl = `/api/image/original${media.backdrop_path}`;\n  }\n\n  $: title = type === 'movie' ? media?.title : media?.name;\n  $: href = `/media/${media?.id}?type=${type}`;\n</script>\n\n<div class=\"relative w-full h-[60vh] min-h-[400px] overflow-hidden\">\n  {#if backdropUrl}\n    <div class=\"absolute inset-0\">\n      <img\n        src={backdropUrl}\n        alt={title}\n        class=\"w-full h-full object-cover\"\n      />\n      <div class=\"absolute inset-0 bg-gradient-to-t from-gray-900 via-gray-900/80\"></div>\n    </div>\n  {/if}\n\n  <div class=\"absolute inset-0 flex items-end\">\n    <div class=\"container mx-auto px-4 pb-16\">\n      <div class=\"max-w-2xl\">\n        <h1 class=\"text-4xl md:text-5xl font-bold text-white mb-4\">\n          {title}\n        </h1>\n        {#if media?.overview}\n          <p class=\"text-lg text-gray-300 mb-6 line-clamp-3\">\n            {media.overview}\n          </p>\n        {/if}\n        <div class=\"flex items-center gap-4\">\n          <a\n            {href}\n            class=\"inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-primary-500 hover:bg-primary-600 text-white font-semibold transition-colors\"\n          >\n            <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z\" />\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n            </svg>\n            Watch Now\n          </a>\n          <div class=\"flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/80 text-white\">\n            <svg class=\"w-5 h-5 text-yellow-400\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n              <path d=\"M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z\" />\n            </svg>\n            <span class=\"font-bold\">{media?.vote_average?.toFixed(1) || '0.0'}</span>\n            <span class=\"text-gray-400\">/ 10</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/lib/components/Image.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n\n  export let src: string | null;\n  export let alt: string;\n  export let class_ = '';\n  export let sizes = '100vw';\n\n  let loaded = false;\n  let error = false;\n  let imageElement: HTMLImageElement;\n\n  const placeholder = '/placeholder.jpg';\n\n  function generateSrcSet(path: string): string {\n    const widths = [300, 500, 700, 900, 1100];\n    return widths\n      .map(width => {\n        const size = width <= 500 ? 'w500' : width <= 700 ? 'w780' : 'original';\n        return `/api/image/${size}${path} ${width}w`;\n      })\n      .join(', ');\n  }\n\n  $: finalSrc = src ? `/api/image/w500${src}` : placeholder;\n  $: srcset = src ? generateSrcSet(src) : '';\n\n  function handleLoad() {\n    loaded = true;\n  }\n\n  function handleError() {\n    error = true;\n    if (imageElement) {\n      imageElement.src = placeholder;\n    }\n  }\n\n  $: if (src) {\n    loaded = false;\n    error = false;\n  }\n</script>\n\n<div class=\"relative overflow-hidden {class_}\">\n  <img\n    bind:this={imageElement}\n    src={finalSrc}\n    {srcset}\n    {sizes}\n    {alt}\n    class=\"w-full h-full object-cover transition-opacity duration-300\"\n    class:opacity-0={!loaded}\n    class:opacity-100={loaded}\n    loading=\"lazy\"\n    on:load={handleLoad}\n    on:error={handleError}\n  />\n\n  {#if !loaded}\n    <div class=\"absolute inset-0 bg-gray-800 animate-pulse\"></div>\n  {/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/MediaCard.svelte",
    "content": "<script lang=\"ts\">\n  import Image from './Image.svelte';\n  import WatchlistButton from './WatchlistButton.svelte';\n  import { getReleaseType } from '$lib/services/release-type';\n  import { onMount } from 'svelte';\n\n  export let id: number;\n  export let type: 'movie' | 'tv';\n  export let title: string;\n  export let posterPath: string | null;\n  export let voteAverage: number;\n  export let showWatchlist = true;\n\n  let releaseType = 'Unknown Quality';\n  let certification = '';\n  let loading = true;\n\n  $: href = `/media/${id}?type=${type}`;\n\n  onMount(async () => {\n    try {\n      const releaseInfo = await getReleaseType(id, type);\n      releaseType = releaseInfo.releaseType;\n      certification = releaseInfo.certifications['US'] || '';\n    } catch (error) {\n      console.error('Error fetching release info:', error);\n    } finally {\n      loading = false;\n    }\n  });\n\n  function handleWatchlistClick(event: MouseEvent) {\n    event.stopPropagation();\n    event.preventDefault();\n  }\n\n  function handleWatchlistKeydown(event: KeyboardEvent) {\n    if (event.key === 'Enter') {\n      event.stopPropagation();\n      event.preventDefault();\n    }\n  }\n</script>\n\n<div class=\"group relative bg-gray-800 rounded-lg overflow-hidden transition-transform hover:scale-105\">\n  <a {href} class=\"block\">\n    <div class=\"aspect-[2/3] relative\">\n      <Image\n        src={posterPath}\n        alt={title}\n        class_=\"w-full h-full\"\n        sizes=\"(min-width: 1280px) 16.666vw, (min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33.333vw, 50vw\"\n      />\n\n      <!-- Rating Badge -->\n      <div class=\"absolute top-2 left-2 flex items-center gap-1 px-2 py-1 rounded bg-black/80\">\n        <svg class=\"w-4 h-4 text-yellow-400\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n          <path d=\"M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z\" />\n        </svg>\n        <span class=\"text-sm font-bold text-white\">{voteAverage.toFixed(1)}</span>\n      </div>\n\n      {#if !loading && releaseType !== 'Unknown Quality'}\n        <div class=\"absolute top-2 right-10 px-2 py-1 text-xs font-semibold rounded bg-primary-500 text-white\">\n          {releaseType}\n        </div>\n      {/if}\n\n      {#if certification}\n        <div class=\"absolute bottom-2 right-2 px-2 py-1 text-xs font-semibold rounded bg-gray-700 text-white\">\n          {certification}\n        </div>\n      {/if}\n\n      <!-- Hover Overlay -->\n      <div class=\"absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center\">\n        <div class=\"text-white text-center p-4\">\n          <span class=\"text-lg font-bold line-clamp-2\">{title}</span>\n          <div class=\"mt-2\">\n            <span class=\"inline-block px-3 py-1 rounded-full bg-primary-500 text-sm\">\n              {type === 'movie' ? 'Movie' : 'TV Show'}\n            </span>\n          </div>\n        </div>\n      </div>\n    </div>\n  </a>\n\n  {#if showWatchlist}\n    <div\n      role=\"button\"\n      tabindex=\"0\"\n      class=\"absolute top-2 right-2 z-10\"\n      on:click={handleWatchlistClick}\n      on:keydown={handleWatchlistKeydown}\n    >\n      <WatchlistButton {id} {type} {title} posterPath={posterPath} {voteAverage} />\n    </div>\n  {/if}\n</div>\n\n<style>\n  .line-clamp-2 {\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/components/MediaFilters.svelte",
    "content": "<script lang=\"ts\">\n  import { createEventDispatcher } from 'svelte';\n\n  export let type: 'movie' | 'tv';\n  export let selectedSort = 'trending';\n  export let selectedGenre = '';\n  export let selectedYear = '';\n\n  const dispatch = createEventDispatcher<{\n    filter: { sort: string; genre: string; year: string };\n  }>();\n\n  const sortOptions = [\n    { value: 'trending', label: 'Trending' },\n    { value: 'popular', label: 'Popular' },\n    { value: 'top_rated', label: 'Top Rated' },\n    { value: 'now_playing', label: type === 'movie' ? 'Now Playing' : 'Currently Airing' },\n    { value: 'upcoming', label: type === 'movie' ? 'Upcoming' : 'Upcoming Shows' }\n  ];\n\n  const genres = [\n    { id: '28', name: 'Action' },\n    { id: '12', name: 'Adventure' },\n    { id: '16', name: 'Animation' },\n    { id: '35', name: 'Comedy' },\n    { id: '80', name: 'Crime' },\n    { id: '99', name: 'Documentary' },\n    { id: '18', name: 'Drama' },\n    { id: '10751', name: 'Family' },\n    { id: '14', name: 'Fantasy' },\n    { id: '36', name: 'History' },\n    { id: '27', name: 'Horror' },\n    { id: '10402', name: 'Music' },\n    { id: '9648', name: 'Mystery' },\n    { id: '10749', name: 'Romance' },\n    { id: '878', name: 'Science Fiction' },\n    { id: '10770', name: 'TV Movie' },\n    { id: '53', name: 'Thriller' },\n    { id: '10752', name: 'War' },\n    { id: '37', name: 'Western' }\n  ];\n\n  const currentYear = new Date().getFullYear();\n  const years = Array.from({ length: 50 }, (_, i) => (currentYear - i).toString());\n\n  function handleChange() {\n    dispatch('filter', {\n      sort: selectedSort,\n      genre: selectedGenre,\n      year: selectedYear\n    });\n  }\n</script>\n\n<div class=\"bg-gray-800 rounded-lg p-4 mb-6\">\n  <div class=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n    <div>\n      <label for=\"sort\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n        Sort By\n      </label>\n      <select\n        id=\"sort\"\n        bind:value={selectedSort}\n        on:change={handleChange}\n        class=\"w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500\"\n      >\n        {#each sortOptions as option}\n          <option value={option.value}>{option.label}</option>\n        {/each}\n      </select>\n    </div>\n\n    <div>\n      <label for=\"genre\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n        Genre\n      </label>\n      <select\n        id=\"genre\"\n        bind:value={selectedGenre}\n        on:change={handleChange}\n        class=\"w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500\"\n      >\n        <option value=\"\">All Genres</option>\n        {#each genres as genre}\n          <option value={genre.id}>{genre.name}</option>\n        {/each}\n      </select>\n    </div>\n\n    <div>\n      <label for=\"year\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n        Year\n      </label>\n      <select\n        id=\"year\"\n        bind:value={selectedYear}\n        on:change={handleChange}\n        class=\"w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500\"\n      >\n        <option value=\"\">All Years</option>\n        {#each years as year}\n          <option value={year}>{year}</option>\n        {/each}\n      </select>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/lib/components/MediaPlayer.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n\n  export let src: string;\n  export let title: string;\n  export let autoplay = false;\n  export let controls = true;\n  export let muted = false;\n\n  let player: HTMLVideoElement;\n  let volume = 1;\n\n  onMount(() => {\n    if (player) {\n      player.volume = volume;\n    }\n  });\n\n  function handleKeyDown(event: KeyboardEvent) {\n    switch (event.code) {\n      case 'Space':\n        event.preventDefault();\n        if (player) {\n          if (player.paused) {\n            player.play();\n          } else {\n            player.pause();\n          }\n        }\n        break;\n      case 'ArrowLeft':\n        event.preventDefault();\n        if (player) player.currentTime -= 5;\n        break;\n      case 'ArrowRight':\n        event.preventDefault();\n        if (player) player.currentTime += 5;\n        break;\n      case 'ArrowUp':\n        event.preventDefault();\n        if (player) player.volume = Math.min(1, player.volume + 0.1);\n        break;\n      case 'ArrowDown':\n        event.preventDefault();\n        if (player) player.volume = Math.max(0, player.volume - 0.1);\n        break;\n    }\n  }\n</script>\n\n<div\n  class=\"relative w-full h-full bg-black focus:outline-none focus:ring-2 focus:ring-primary-500\"\n  on:keydown={handleKeyDown}\n  role=\"application\"\n  aria-label=\"Video player for {title}\"\n>\n  <video\n    bind:this={player}\n    {src}\n    class=\"w-full h-full\"\n    {autoplay}\n    {controls}\n    {muted}\n  >\n    <track kind=\"captions\" />\n  </video>\n</div>\n"
  },
  {
    "path": "src/lib/components/MentionList.svelte",
    "content": "<script lang=\"ts\">\n  import { createEventDispatcher } from 'svelte';\n\n  export let items: { id: number; username: string }[] = [];\n  export let command: (props: { id: number; label: string }) => void;\n\n  let selectedIndex = 0;\n  const dispatch = createEventDispatcher();\n\n  function onKeyDown(event: KeyboardEvent) {\n    if (event.key === 'ArrowUp') {\n      event.preventDefault();\n      upHandler();\n      return true;\n    }\n\n    if (event.key === 'ArrowDown') {\n      event.preventDefault();\n      downHandler();\n      return true;\n    }\n\n    if (event.key === 'Enter') {\n      event.preventDefault();\n      enterHandler();\n      return true;\n    }\n\n    return false;\n  }\n\n  function upHandler() {\n    selectedIndex = (selectedIndex + items.length - 1) % items.length;\n  }\n\n  function downHandler() {\n    selectedIndex = (selectedIndex + 1) % items.length;\n  }\n\n  function enterHandler() {\n    selectItem(selectedIndex);\n  }\n\n  function selectItem(index: number) {\n    const item = items[index];\n    if (item) {\n      command({ id: item.id, label: item.username });\n    }\n  }\n\n  dispatch('keydown', { onKeyDown });\n</script>\n\n<div class=\"mention-list bg-gray-800 rounded-lg shadow-lg overflow-hidden\">\n  {#each items as item, index}\n    <button\n      class=\"w-full px-4 py-2 text-left hover:bg-gray-700 transition-colors\"\n      class:bg-gray-700={index === selectedIndex}\n      on:click={() => selectItem(index)}\n    >\n      <div class=\"flex items-center gap-2\">\n        <div class=\"w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center\">\n          <span class=\"text-sm font-medium\">\n            {item.username[0].toUpperCase()}\n          </span>\n        </div>\n        <span class=\"text-sm\">{item.username}</span>\n      </div>\n    </button>\n  {/each}\n</div>\n\n<style>\n  .mention-list {\n    max-height: 200px;\n    overflow-y: auto;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/components/Navbar.svelte",
    "content": "<script lang=\"ts\">\n  import { page } from '$app/stores';\n  import { onMount } from 'svelte';\n  import { authStore } from '$lib/stores/auth';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  let isScrolled = false;\n  let isMobileMenuOpen = false;\n  let isUserMenuOpen = false;\n\n  onMount(() => {\n    const handleScroll = () => {\n      isScrolled = window.scrollY > 0;\n    };\n\n    window.addEventListener('scroll', handleScroll);\n    return () => window.removeEventListener('scroll', handleScroll);\n  });\n\n  const navItems = [\n    { href: '/', label: 'Home' },\n    { href: '/movies', label: 'Movies' },\n    { href: '/tv', label: 'TV Shows' },\n    { href: '/watchlist', label: 'Watchlist', requiresAuth: true }\n  ];\n\n  async function handleLogout() {\n    try {\n      await csrfFetch('/api/auth/logout', { method: 'POST' });\n      window.location.href = '/';\n    } catch (error) {\n      console.error('Logout failed:', error);\n    }\n  }\n</script>\n\n<nav\n  class=\"fixed top-0 left-0 right-0 z-50 transition-colors duration-300\"\n  class:bg-gray-900={isScrolled}\n  class:backdrop-blur={isScrolled}\n>\n  <div class=\"container mx-auto px-4\">\n    <div class=\"flex items-center justify-between h-16\">\n      <!-- Logo -->\n      <a href=\"/\" class=\"flex items-center gap-2\">\n        <span class=\"text-2xl font-bold text-primary-400\">Streamium</span>\n      </a>\n\n      <!-- Desktop Navigation -->\n      <div class=\"hidden md:flex items-center gap-6\">\n        {#each navItems as item}\n          {#if !item.requiresAuth || $authStore.isAuthenticated}\n            <a\n              href={item.href}\n              class=\"text-gray-300 hover:text-white transition-colors\"\n              class:text-primary-400={$page.url.pathname === item.href}\n            >\n              {item.label}\n            </a>\n          {/if}\n        {/each}\n      </div>\n\n      <!-- Search and User Menu -->\n      <div class=\"flex items-center gap-4\">\n        <a\n          href=\"/search\"\n          class=\"p-2 text-gray-300 hover:text-white transition-colors\"\n          aria-label=\"Search\"\n        >\n          <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n          </svg>\n        </a>\n\n        {#if $authStore.isAuthenticated}\n          <!-- User Menu -->\n          <div class=\"relative\">\n            <button\n              type=\"button\"\n              class=\"flex items-center gap-2 p-2 text-gray-300 hover:text-white transition-colors\"\n              on:click={() => isUserMenuOpen = !isUserMenuOpen}\n              aria-label=\"User menu\"\n            >\n              <span class=\"text-sm\">{$authStore.user?.username}</span>\n              <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\" />\n              </svg>\n            </button>\n\n            {#if isUserMenuOpen}\n              <div class=\"absolute right-0 mt-2 w-48 py-2 bg-gray-800 rounded-lg shadow-xl\">\n                {#if $authStore.user?.isAdmin}\n                  <a\n                    href=\"/admin/moderation\"\n                    class=\"block px-4 py-2 text-gray-300 hover:text-white hover:bg-gray-700\"\n                    on:click={() => isUserMenuOpen = false}\n                  >\n                    <div class=\"flex items-center gap-2\">\n                      <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\" />\n                      </svg>\n                      <span>Moderation</span>\n                    </div>\n                  </a>\n                {/if}\n                <button\n                  type=\"button\"\n                  class=\"w-full text-left px-4 py-2 text-gray-300 hover:text-white hover:bg-gray-700\"\n                  on:click={handleLogout}\n                >\n                  Logout\n                </button>\n              </div>\n            {/if}\n          </div>\n        {:else}\n          <!-- Auth Links -->\n          <div class=\"hidden md:flex items-center gap-4\">\n            <a\n              href=\"/login\"\n              class=\"text-gray-300 hover:text-white transition-colors\"\n            >\n              Login\n            </a>\n            <a\n              href=\"/register\"\n              class=\"px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors\"\n            >\n              Register\n            </a>\n          </div>\n        {/if}\n\n        <!-- Mobile Menu Button -->\n        <button\n          type=\"button\"\n          class=\"md:hidden p-2 text-gray-300 hover:text-white transition-colors\"\n          on:click={() => isMobileMenuOpen = !isMobileMenuOpen}\n          aria-label=\"Toggle mobile menu\"\n        >\n          <svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n            {#if isMobileMenuOpen}\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n            {:else}\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 12h16M4 18h16\" />\n            {/if}\n          </svg>\n        </button>\n      </div>\n    </div>\n\n    <!-- Mobile Navigation -->\n    {#if isMobileMenuOpen}\n      <div class=\"md:hidden py-4 space-y-2\">\n        {#each navItems as item}\n          {#if !item.requiresAuth || $authStore.isAuthenticated}\n            <a\n              href={item.href}\n              class=\"block px-4 py-2 text-gray-300 hover:text-white transition-colors\"\n              class:text-primary-400={$page.url.pathname === item.href}\n              on:click={() => isMobileMenuOpen = false}\n            >\n              {item.label}\n            </a>\n          {/if}\n        {/each}\n\n        {#if $authStore.isAuthenticated && $authStore.user?.isAdmin}\n          <a\n            href=\"/admin/moderation\"\n            class=\"block px-4 py-2 text-gray-300 hover:text-white transition-colors\"\n            on:click={() => isMobileMenuOpen = false}\n          >\n            Moderation\n          </a>\n        {/if}\n\n        {#if !$authStore.isAuthenticated}\n          <div class=\"pt-4 border-t border-gray-700\">\n            <a\n              href=\"/login\"\n              class=\"block px-4 py-2 text-gray-300 hover:text-white transition-colors\"\n              on:click={() => isMobileMenuOpen = false}\n            >\n              Login\n            </a>\n            <a\n              href=\"/register\"\n              class=\"block px-4 py-2 text-gray-300 hover:text-white transition-colors\"\n              on:click={() => isMobileMenuOpen = false}\n            >\n              Register\n            </a>\n          </div>\n        {/if}\n      </div>\n    {/if}\n  </div>\n</nav>\n\n<!-- Spacer to prevent content from being hidden under fixed navbar -->\n<div class=\"h-16\"></div>\n"
  },
  {
    "path": "src/lib/components/NextEpisode.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n\n  export let mediaId: number;\n  export let currentSeason: number;\n  export let currentEpisode: number;\n  export let onSelect: (season: number, episode: number) => void;\n\n  interface Episode {\n    name: string;\n    episode_number: number;\n    season_number?: number;\n  }\n\n  interface Season {\n    season_number: number;\n  }\n\n  let nextEpisode: Episode | null = null;\n  let loading = true;\n\n  async function loadNextEpisode() {\n    loading = true;\n    try {\n\n      const response = await fetch(`/api/tv/${mediaId}/season/${currentSeason}`);\n      if (response.ok) {\n        const data = await response.json();\n        const episodes = data.episodes || [];\n        const nextInSeason = episodes.find((ep: Episode) => ep.episode_number === currentEpisode + 1);\n\n        if (nextInSeason) {\n          nextEpisode = { ...nextInSeason, season_number: currentSeason };\n        } else {\n\n          const seasonsResponse = await fetch(`/api/tv/${mediaId}/seasons`);\n          if (seasonsResponse.ok) {\n            const seasonsData = await seasonsResponse.json();\n            const seasons = seasonsData.seasons.filter((s: Season) => s.season_number > 0);\n            const nextSeason = seasons.find((s: Season) => s.season_number === currentSeason + 1);\n\n            if (nextSeason) {\n              const nextSeasonResponse = await fetch(`/api/tv/${mediaId}/season/${nextSeason.season_number}`);\n              if (nextSeasonResponse.ok) {\n                const nextSeasonData = await nextSeasonResponse.json();\n                const firstEpisode = nextSeasonData.episodes[0];\n                if (firstEpisode) {\n                  nextEpisode = { ...firstEpisode, season_number: nextSeason.season_number };\n                }\n              }\n            }\n          }\n        }\n      }\n    } catch (error) {\n      console.error('Error loading next episode:', error);\n    } finally {\n      loading = false;\n    }\n  }\n\n  function handleNextEpisode() {\n    if (nextEpisode && nextEpisode.season_number) {\n      onSelect(nextEpisode.season_number, nextEpisode.episode_number);\n    }\n  }\n\n  onMount(() => {\n    if (mediaId && currentSeason && currentEpisode) {\n      loadNextEpisode();\n    }\n  });\n\n  $: if (mediaId && currentSeason && currentEpisode) {\n    if (typeof window !== 'undefined') {\n      loadNextEpisode();\n    }\n  }\n</script>\n\n{#if !loading && nextEpisode}\n  <button\n    class=\"px-4 py-2 bg-gray-700 text-white rounded-lg font-medium hover:bg-gray-600 transition-colors whitespace-nowrap flex items-center gap-2\"\n    on:click={handleNextEpisode}\n  >\n    <span>Next: {nextEpisode.season_number !== currentSeason ? `S${nextEpisode.season_number}E${nextEpisode.episode_number}` : `E${nextEpisode.episode_number}`}</span>\n    <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n      <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n    </svg>\n  </button>\n{/if}\n"
  },
  {
    "path": "src/lib/components/Pagination.svelte",
    "content": "<script lang=\"ts\">\n  import { filters } from '$lib/stores/filters';\n\n  export let totalPages: number;\n  export let currentPage: number;\n  export let onPageChange: (page: number) => void;\n\n  $: pages = generatePageNumbers(currentPage, totalPages);\n\n  function generatePageNumbers(current: number, total: number) {\n    const pages: (number | string)[] = [];\n    const maxVisiblePages = 7;\n\n    if (total <= maxVisiblePages) {\n      return Array.from({ length: total }, (_, i) => i + 1);\n    }\n\n\n    pages.push(1);\n\n\n    let start = Math.max(2, current - 2);\n    let end = Math.min(total - 1, current + 2);\n\n\n    if (current <= 4) {\n      end = 5;\n    }\n\n\n    if (current >= total - 3) {\n      start = total - 4;\n    }\n\n\n    if (start > 2) {\n      pages.push('...');\n    }\n\n\n    for (let i = start; i <= end; i++) {\n      pages.push(i);\n    }\n\n\n    if (end < total - 1) {\n      pages.push('...');\n    }\n\n\n    pages.push(total);\n\n    return pages;\n  }\n\n  function handlePageChange(page: number) {\n    if (page === currentPage) return;\n    onPageChange(page);\n\n    window.scrollTo({ top: 0, behavior: 'smooth' });\n  }\n</script>\n\n<nav class=\"flex justify-center mt-8\" aria-label=\"Pagination\">\n  <ul class=\"flex items-center gap-1\">\n    <!-- Previous Button -->\n    <li>\n      <button\n        class=\"px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed\"\n        disabled={currentPage === 1}\n        on:click={() => handlePageChange(currentPage - 1)}\n        aria-label=\"Previous page\"\n      >\n        <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n        </svg>\n      </button>\n    </li>\n\n    <!-- Page Numbers -->\n    {#each pages as page}\n      <li>\n        {#if typeof page === 'string'}\n          <span class=\"px-4 py-2 text-gray-400\">\n            {page}\n          </span>\n        {:else}\n          <button\n            class=\"min-w-[40px] px-4 py-2 rounded-lg {currentPage === page\n              ? 'bg-purple-600 text-white'\n              : 'text-gray-400 hover:text-white hover:bg-gray-700'}\"\n            on:click={() => handlePageChange(page)}\n            aria-label=\"Page {page}\"\n            aria-current={currentPage === page ? 'page' : undefined}\n          >\n            {page}\n          </button>\n        {/if}\n      </li>\n    {/each}\n\n    <!-- Next Button -->\n    <li>\n      <button\n        class=\"px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed\"\n        disabled={currentPage === totalPages}\n        on:click={() => handlePageChange(currentPage + 1)}\n        aria-label=\"Next page\"\n      >\n        <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n        </svg>\n      </button>\n    </li>\n  </ul>\n</nav>\n\n{#if totalPages > 1}\n  <div class=\"text-center mt-2 text-sm text-gray-400\">\n    Page {currentPage} of {totalPages}\n  </div>\n{/if}\n"
  },
  {
    "path": "src/lib/components/ReplyForm.svelte",
    "content": "<script lang=\"ts\">\n  import RichTextEditor from './RichTextEditor.svelte';\n  import { validateComment } from '$lib/shared/comment-validation';\n  import { authStore } from '$lib/stores/auth';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  interface User {\n    id: number;\n    username: string;\n  }\n\n  export let mediaId: number;\n  export let mediaType: 'movie' | 'tv';\n  export let season: number | undefined = undefined;\n  export let episode: number | undefined = undefined;\n  export let parentId: number;\n  export let onReplyAdded: (reply: {\n    id: number;\n    content: string;\n    createdAt: string;\n    user: User;\n    replies: never[];\n    _count: { likes: number };\n    isLiked: boolean;\n    flagged: boolean;\n    parentId: number;\n  }) => void;\n  export let onCancel: () => void;\n\n  let content = '<p></p>';\n  let isSubmitting = false;\n  let error = '';\n  let charCount = 0;\n  let isValid = false;\n  let editor: RichTextEditor;\n  const MAX_CHARS = 1000;\n\n  $: {\n    if (content === '<p></p>') {\n      isValid = false;\n      error = '';\n      charCount = 0;\n    } else {\n      const validation = validateComment(content);\n      isValid = validation.isValid && charCount <= MAX_CHARS;\n      if (!validation.isValid && validation.error) {\n        error = validation.error;\n      } else if (charCount > MAX_CHARS) {\n        error = 'Reply is too long';\n      } else {\n        error = '';\n      }\n    }\n  }\n\n  async function handleSubmit() {\n    if (!isValid || isSubmitting) return;\n\n    try {\n      isSubmitting = true;\n      error = '';\n\n      const validation = validateComment(content);\n      if (!validation.isValid) {\n        error = validation.error || 'Invalid reply';\n        return;\n      }\n\n      const response = await csrfFetch('/api/comments', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          mediaId,\n          mediaType,\n          content,\n          parentId,\n          season: mediaType === 'tv' ? season : undefined,\n          episode: mediaType === 'tv' ? episode : undefined\n        })\n      });\n\n      if (!response.ok) {\n        const data = await response.json();\n        throw new Error(data.error || 'Failed to post reply');\n      }\n\n      const newReply = await response.json();\n      editor?.clear();\n      onReplyAdded({\n        ...newReply,\n        user: $authStore.user!,\n        replies: [],\n        _count: { likes: 0 },\n        isLiked: false,\n        flagged: false\n      });\n    } catch (err) {\n      if (err instanceof Error) {\n        error = err.message;\n      } else {\n        error = 'An unexpected error occurred';\n      }\n    } finally {\n      isSubmitting = false;\n    }\n  }\n\n  function handleContentInput(event: CustomEvent<string>) {\n    content = event.detail;\n\n    const textContent = content.replace(/<[^>]*>/g, '');\n    charCount = textContent.length;\n  }\n</script>\n\n<form on:submit|preventDefault={handleSubmit} class=\"space-y-4\">\n  <div class=\"space-y-2\">\n    <RichTextEditor\n      bind:this={editor}\n      bind:content\n      on:input={handleContentInput}\n      class_=\"min-h-[100px] bg-gray-900/50\"\n    />\n\n    <div class=\"flex justify-between items-center text-sm\">\n      <span class=\"text-gray-400\">\n        {charCount}/{MAX_CHARS} characters\n      </span>\n      {#if error}\n        <span class=\"text-red-400\">{error}</span>\n      {/if}\n    </div>\n  </div>\n\n  <div class=\"flex justify-end gap-2\">\n    <button\n      type=\"button\"\n      class=\"px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors\"\n      on:click={onCancel}\n    >\n      Cancel\n    </button>\n    <button\n      type=\"submit\"\n      class=\"px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n      disabled={!isValid || isSubmitting}\n    >\n      {isSubmitting ? 'Posting...' : 'Post Reply'}\n    </button>\n  </div>\n</form>\n"
  },
  {
    "path": "src/lib/components/RichTextEditor.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount, onDestroy, createEventDispatcher } from 'svelte';\n  import { Editor } from '@tiptap/core';\n  import StarterKit from '@tiptap/starter-kit';\n  import EmojiPicker from './EmojiPicker.svelte';\n\n  const dispatch = createEventDispatcher<{ input: string }>();\n  const EMPTY_CONTENT = '<p></p>';\n\n  export let content = EMPTY_CONTENT;\n  export let disabled = false;\n  export let class_ = '';\n\n  let element: HTMLDivElement;\n  let editor: Editor;\n\n  onMount(() => {\n    editor = new Editor({\n      element,\n      extensions: [\n        StarterKit.configure({\n          heading: false,\n          bulletList: false,\n          orderedList: false,\n          code: false,\n          codeBlock: false,\n          blockquote: false,\n          horizontalRule: false,\n          hardBreak: false,\n          history: {},\n        }),\n      ],\n      content: EMPTY_CONTENT,\n      editable: !disabled,\n      onUpdate: ({ editor }) => {\n        content = editor.getHTML();\n        dispatch('input', content);\n      },\n      editorProps: {\n        attributes: {\n          class: 'prose prose-invert max-w-none min-h-[120px] p-4 focus:outline-none',\n        },\n      },\n    });\n\n    return () => {\n      editor.destroy();\n    };\n  });\n\n  onDestroy(() => {\n    if (editor) {\n      editor.destroy();\n    }\n  });\n\n  export function clear() {\n    if (editor) {\n      editor.commands.setContent(EMPTY_CONTENT);\n      content = EMPTY_CONTENT;\n      dispatch('input', content);\n    }\n  }\n\n  function handleEmojiSelect(event: CustomEvent<string>) {\n    if (editor) {\n      editor.commands.insertContent(event.detail);\n    }\n  }\n\n  $: if (editor && disabled !== undefined) {\n    editor.setEditable(!disabled);\n  }\n</script>\n\n<div class=\"relative {class_} bg-transparent rounded-lg border border-gray-700/50\">\n  <div\n    bind:this={element}\n    class=\"prose-sm prose-invert\"\n  ></div>\n\n  <div class=\"border-t border-gray-700/50 p-2 flex gap-2\">\n    <button\n      type=\"button\"\n      class=\"p-1 rounded hover:bg-gray-700/50 transition-colors\"\n      class:text-blue-400={editor?.isActive('bold')}\n      on:click={() => editor?.chain().focus().toggleBold().run()}\n      disabled={disabled}\n      aria-label=\"Bold\"\n      title=\"Bold\"\n    >\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n        <path d=\"M15.6 11.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 7.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z\"/>\n      </svg>\n    </button>\n\n    <button\n      type=\"button\"\n      class=\"p-1 rounded hover:bg-gray-700/50 transition-colors\"\n      class:text-blue-400={editor?.isActive('italic')}\n      on:click={() => editor?.chain().focus().toggleItalic().run()}\n      disabled={disabled}\n      aria-label=\"Italic\"\n      title=\"Italic\"\n    >\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n        <path d=\"M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z\"/>\n      </svg>\n    </button>\n\n    <button\n      type=\"button\"\n      class=\"p-1 rounded hover:bg-gray-700/50 transition-colors\"\n      class:text-blue-400={editor?.isActive('strike')}\n      on:click={() => editor?.chain().focus().toggleStrike().run()}\n      disabled={disabled}\n      aria-label=\"Strikethrough\"\n      title=\"Strikethrough\"\n    >\n      <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n        <path d=\"M7.24 8.75c-.26-.48-.39-1.03-.39-1.67 0-.61.13-1.16.4-1.67.26-.5.63-.93 1.11-1.29.48-.35 1.05-.63 1.7-.83.66-.19 1.39-.29 2.18-.29.81 0 1.54.11 2.21.34.66.22 1.23.54 1.69.94.47.4.83.88 1.08 1.43.25.55.38 1.15.38 1.81h-3.01c0-.31-.05-.59-.15-.85-.09-.27-.24-.49-.44-.68-.2-.19-.45-.33-.75-.44-.3-.1-.66-.16-1.06-.16-.39 0-.74.04-1.03.13-.29.09-.53.21-.72.36-.19.16-.34.34-.44.55-.1.21-.15.43-.15.66 0 .48.25.88.74 1.21.38.25.77.48 1.41.7H7.39c-.05-.08-.11-.17-.15-.25zM21 12v-2H3v2h9.62c.18.07.4.14.55.2.37.17.66.34.87.51.21.17.35.36.43.57.07.2.11.43.11.69 0 .23-.05.45-.14.66-.09.2-.23.38-.42.53-.19.15-.42.26-.71.35-.29.08-.63.13-1.01.13-.43 0-.83-.04-1.18-.13s-.66-.23-.91-.42c-.25-.19-.45-.44-.59-.75-.14-.31-.25-.76-.25-1.21H6.4c0 .55.08 1.13.24 1.58.16.45.37.85.65 1.21.28.35.6.66.98.92.37.26.78.48 1.22.65.44.17.9.3 1.38.39.48.08.96.13 1.44.13.8 0 1.53-.09 2.18-.28s1.21-.45 1.67-.79c.46-.34.82-.77 1.07-1.27s.38-1.07.38-1.71c0-.6-.1-1.14-.31-1.61-.05-.11-.11-.23-.17-.33H21z\"/>\n      </svg>\n    </button>\n\n    <div class=\"flex-1\"></div>\n\n    <EmojiPicker\n      on:select={handleEmojiSelect}\n      disabled={disabled}\n    />\n  </div>\n</div>\n\n<style>\n  :global(.ProseMirror) {\n    outline: none;\n  }\n\n  :global(.ProseMirror p.is-editor-empty:first-child::before) {\n    content: attr(data-placeholder);\n    float: left;\n    color: #9ca3af;\n    pointer-events: none;\n    height: 0;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/components/Toast.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import { fade, fly } from 'svelte/transition';\n  import { toastStore, type Toast } from '$lib/stores/toast';\n\n  let toasts: Toast[] = [];\n  toastStore.subscribe(value => {\n    toasts = value;\n  });\n\n  const icons = {\n    success: `<svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n      <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n    </svg>`,\n    error: `<svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n      <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n    </svg>`,\n    warning: `<svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n      <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n    </svg>`,\n    info: `<svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n      <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n    </svg>`\n  };\n\n  const colors = {\n    success: 'bg-green-500',\n    error: 'bg-red-500',\n    warning: 'bg-yellow-500',\n    info: 'bg-blue-500'\n  };\n\n  function removeToast(id: string) {\n    toastStore.remove(id);\n  }\n</script>\n\n<div class=\"fixed bottom-4 right-4 z-50 flex flex-col gap-2\">\n  {#each toasts as toast (toast.id)}\n    <div\n      class=\"flex items-center gap-3 min-w-[300px] max-w-md p-4 text-white rounded-lg shadow-lg\"\n      class:bg-green-500={toast.type === 'success'}\n      class:bg-red-500={toast.type === 'error'}\n      class:bg-yellow-500={toast.type === 'warning'}\n      class:bg-blue-500={toast.type === 'info'}\n      transition:fly={{ y: 50, duration: 200 }}\n      role=\"alert\"\n    >\n      <div class=\"flex-shrink-0\">\n        {@html icons[toast.type]}\n      </div>\n      <p class=\"flex-1\">{toast.message}</p>\n      <button\n        class=\"flex-shrink-0 hover:opacity-75\"\n        on:click={() => removeToast(toast.id)}\n        aria-label=\"Close notification\"\n      >\n        <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n        </svg>\n      </button>\n    </div>\n  {/each}\n</div>\n"
  },
  {
    "path": "src/lib/components/VideoPlayer.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount, onDestroy } from 'svelte';\n  import { browser } from '$app/environment';\n  import { providers, getDefaultProvider, type Provider } from '$lib/services/providers';\n\n  export let mediaId: string | number;\n  export let mediaType: 'movie' | 'tv';\n  export let title: string;\n  export let season: number | undefined = undefined;\n  export let episode: number | undefined = undefined;\n\n  let selectedProvider = getDefaultProvider();\n  let iframe: HTMLIFrameElement;\n  let loading = true;\n  let error: string | null = null;\n  let retryCount = 0;\n  const MAX_RETRIES = providers.length;\n\n  $: embedUrl = selectedProvider.getEmbedUrl(mediaId, mediaType, season, episode);\n\n  onMount(() => {\n    if (browser) {\n      window.addEventListener('message', handleProviderMessage);\n    }\n  });\n\n  onDestroy(() => {\n    if (browser) {\n      window.removeEventListener('message', handleProviderMessage);\n    }\n  });\n\n  function handleProviderMessage(event: MessageEvent) {\n    if (event.data?.type === 'error') {\n      handleError();\n    }\n  }\n\n  export function changeProvider(providerId: string) {\n    const newProvider = providers.find(p => p.id === providerId);\n    if (newProvider) {\n      selectedProvider = newProvider;\n      localStorage.setItem('selectedProvider', newProvider.id);\n      loading = true;\n      error = null;\n      retryCount = 0;\n    }\n  }\n\n  function handleIframeLoad() {\n    loading = false;\n    retryCount = 0;\n  }\n\n  function handleError() {\n    if (retryCount < MAX_RETRIES) {\n      retryCount++;\n      tryNextProvider();\n    } else {\n      loading = false;\n      error = 'Failed to load video player after trying all providers. Please try again later.';\n    }\n  }\n\n  function handleIframeError() {\n    handleError();\n  }\n\n  function tryNextProvider() {\n    const currentIndex = providers.indexOf(selectedProvider);\n    const nextIndex = (currentIndex + 1) % providers.length;\n    selectedProvider = providers[nextIndex];\n    localStorage.setItem('selectedProvider', selectedProvider.id);\n    loading = true;\n    error = null;\n  }\n</script>\n\n<div class=\"video-player-frame relative w-full bg-black rounded-lg overflow-hidden\">\n  {#if loading}\n    <div class=\"absolute inset-0 flex items-center justify-center bg-gray-900 z-20\">\n      <div class=\"animate-spin rounded-full h-12 w-12 border-4 border-primary-500 border-t-transparent\"></div>\n    </div>\n  {/if}\n\n  <iframe\n    bind:this={iframe}\n    {title}\n    src={embedUrl}\n    class=\"absolute top-0 left-0 w-full h-full\"\n    frameborder=\"0\"\n    scrolling=\"no\"\n    allowfullscreen={true}\n    allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen\"\n    loading=\"lazy\"\n    on:load={handleIframeLoad}\n    on:error={handleIframeError}\n  ></iframe>\n\n  {#if error}\n    <div class=\"absolute inset-0 flex items-center justify-center bg-gray-900 z-20\">\n      <div class=\"text-red-500 text-center p-4\">\n        <p class=\"mb-2\">{error}</p>\n        <button\n          type=\"button\"\n          class=\"px-4 py-2 bg-primary-500 text-white rounded hover:bg-primary-600 transition-colors\"\n          on:click={() => {\n            retryCount = 0;\n            tryNextProvider();\n          }}\n        >\n          Try Different Provider\n        </button>\n      </div>\n    </div>\n  {/if}\n</div>\n\n<style>\n  .video-player-frame {\n    padding-top: 56.25%;\n  }\n</style>\n"
  },
  {
    "path": "src/lib/components/WatchlistButton.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import { browser } from '$app/environment';\n  import { watchlistStore } from '$lib/stores/watchlist';\n  import { toastStore } from '$lib/stores/toast';\n  import { authStore } from '$lib/stores/auth';\n\n  export let id: number;\n  export let type: string;\n  export let title: string;\n  export let posterPath: string | null;\n  export let voteAverage: number;\n\n  let inWatchlist = false;\n  let loading = false;\n  let mounted = false;\n\n  async function checkWatchlistStatus() {\n    if (!browser || !mounted || !$authStore.user) return;\n\n    try {\n      inWatchlist = await watchlistStore.isInWatchlist(id, type);\n    } catch (error) {\n      console.error('Failed to check watchlist status:', error);\n    }\n  }\n\n  async function toggleWatchlist() {\n    if (loading || !$authStore.user) {\n      if (!$authStore.user) {\n        toastStore.error('Please login to add to watchlist');\n      }\n      return;\n    }\n\n    loading = true;\n\n    try {\n      if (inWatchlist) {\n        await watchlistStore.removeFromWatchlist(id, type);\n        toastStore.success('Removed from watchlist');\n      } else {\n        await watchlistStore.addToWatchlist(id, type, title, posterPath, voteAverage);\n        toastStore.success('Added to watchlist');\n      }\n      inWatchlist = !inWatchlist;\n    } catch (error) {\n      console.error('Failed to update watchlist:', error);\n      toastStore.error('Failed to update watchlist');\n    } finally {\n      loading = false;\n    }\n  }\n\n  onMount(() => {\n    mounted = true;\n    checkWatchlistStatus();\n  });\n\n  $: if ($authStore.user) {\n    checkWatchlistStatus();\n  }\n</script>\n\n<button\n  type=\"button\"\n  class=\"p-2 rounded-full bg-gray-900/80 hover:bg-gray-900 transition-colors\"\n  on:click={toggleWatchlist}\n  disabled={loading}\n  aria-label={inWatchlist ? `Remove ${title} from watchlist` : `Add ${title} to watchlist`}\n>\n  <svg\n    class=\"w-5 h-5\"\n    class:text-primary-400={inWatchlist}\n    class:text-gray-400={!inWatchlist}\n    fill=\"currentColor\"\n    viewBox=\"0 0 20 20\"\n  >\n    {#if inWatchlist}\n      <path\n        d=\"M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z\"\n      />\n    {:else}\n      <path\n        d=\"M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        stroke-width=\"2\"\n        stroke-linejoin=\"round\"\n      />\n    {/if}\n  </svg>\n</button>\n"
  },
  {
    "path": "src/lib/constants/security.ts",
    "content": "export const CSRF_COOKIE_NAME = \"csrf\";\nexport const CSRF_HEADER_NAME = \"x-csrf-token\";\n"
  },
  {
    "path": "src/lib/extensions/mention.ts",
    "content": "import { Extension, type Editor, type Range } from \"@tiptap/core\";\nimport Suggestion, { type SuggestionOptions } from \"@tiptap/suggestion\";\n\ntype HTMLAttrs = Record<string, string | number | boolean | null | undefined>;\n\ninterface MentionNode {\n  attrs: {\n    id?: string | null;\n    label?: string | null;\n  };\n}\n\nexport interface MentionOptions {\n  HTMLAttributes?: HTMLAttrs;\n  renderLabel?: (props: { options: MentionOptions; node: MentionNode }) => string;\n  suggestion?: Partial<SuggestionOptions>;\n}\n\ninterface CommandProps {\n  id: number;\n  label: string;\n}\n\nexport const Mention = Extension.create<MentionOptions>({\n  name: \"mention\",\n\n  addOptions() {\n    return {\n      HTMLAttributes: {},\n      renderLabel({ options, node }) {\n        return `@${node.attrs.label ?? \"\"}`;\n      },\n      suggestion: {\n        char: \"@\",\n        command: ({\n          editor,\n          range,\n          props,\n        }: {\n          editor: Editor;\n          range: Range;\n          props: CommandProps;\n        }) => {\n          editor\n            .chain()\n            .focus()\n            .insertContentAt(range, [\n              {\n                type: \"text\",\n                text: `@${props.label}`,\n                marks: [\n                  {\n                    type: \"mention\",\n                    attrs: { id: props.id, label: props.label },\n                  },\n                ],\n              },\n              {\n                type: \"text\",\n                text: \" \",\n              },\n            ])\n            .run();\n        },\n        allow: ({ editor, range }: { editor: Editor; range: Range }) => {\n          return editor.can().insertContentAt(range, {});\n        },\n      },\n    };\n  },\n\n  addAttributes() {\n    return {\n      id: {\n        default: null,\n        parseHTML: (element: HTMLElement) =>\n          element.getAttribute(\"data-mention-id\"),\n        renderHTML: (attributes: { id?: string | null }) => {\n          if (!attributes.id) {\n            return {};\n          }\n\n          return {\n            \"data-mention-id\": attributes.id,\n          };\n        },\n      },\n      label: {\n        default: null,\n        parseHTML: (element: HTMLElement) =>\n          element.getAttribute(\"data-mention-label\"),\n        renderHTML: (attributes: { label?: string | null }) => {\n          if (!attributes.label) {\n            return {};\n          }\n\n          return {\n            \"data-mention-label\": attributes.label,\n          };\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"span[data-mention]\",\n      },\n    ];\n  },\n\n  renderHTML({\n    node,\n    HTMLAttributes,\n  }: {\n    node: MentionNode;\n    HTMLAttributes: HTMLAttrs;\n  }) {\n    return [\n      \"span\",\n      {\n        \"data-mention\": \"\",\n        class: \"mention text-primary-400\",\n        ...this.options.HTMLAttributes,\n        ...HTMLAttributes,\n      },\n      this.options.renderLabel({\n        options: this.options,\n        node,\n      }),\n    ];\n  },\n\n  addProseMirrorPlugins() {\n    return [\n      Suggestion({\n        editor: this.editor,\n        ...this.options.suggestion,\n      }),\n    ];\n  },\n});\n"
  },
  {
    "path": "src/lib/index.ts",
    "content": "// place files you want to import through the `$lib` alias in this folder.\n"
  },
  {
    "path": "src/lib/server/admin-middleware.ts",
    "content": "import type { RequestEvent } from \"@sveltejs/kit\";\nimport { error } from \"@sveltejs/kit\";\nimport { CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from \"$lib/constants/security\";\n\nexport async function requireAdmin(event: RequestEvent) {\n  const user = event.locals.user;\n\n  if (!user) {\n    throw error(401, \"Authentication required\");\n  }\n\n  if (!user.isAdmin) {\n    throw error(403, \"Admin access required\");\n  }\n\n\n  const origin = event.request.headers.get('origin');\n  if (origin && origin !== event.url.origin) {\n    throw error(403, \"Cross-origin requests not allowed\");\n  }\n\n\n  if (event.request.method !== 'GET') {\n    const csrfToken = event.request.headers.get(CSRF_HEADER_NAME);\n    const csrfCookie = event.cookies.get(CSRF_COOKIE_NAME);\n\n    if (!csrfToken || !csrfCookie || csrfToken !== csrfCookie) {\n      throw error(403, \"Invalid CSRF token\");\n    }\n  }\n\n\n  if (['POST', 'PUT', 'PATCH'].includes(event.request.method)) {\n    const contentType = event.request.headers.get('content-type');\n    if (!contentType?.includes('application/json')) {\n      throw error(415, \"Content type must be application/json\");\n    }\n  }\n}\n"
  },
  {
    "path": "src/lib/server/auth.ts",
    "content": "import type { User } from \"@prisma/client\";\nimport jwt from \"jsonwebtoken\";\nimport type { Cookies } from \"@sveltejs/kit\";\nimport { JWT_SECRET } from \"$env/static/private\";\nimport crypto from \"node:crypto\";\nimport { CSRF_COOKIE_NAME } from \"$lib/constants/security\";\nconst COOKIE_NAME = \"session\";\nconst SESSION_MAX_AGE = 60 * 60 * 24 * 7;\n\ninterface Session {\n  userId: number;\n  exp: number;\n}\n\nexport async function createSession(user: User): Promise<string> {\n  const token = jwt.sign(\n    { userId: user.id, exp: Math.floor(Date.now() / 1000) + SESSION_MAX_AGE },\n    JWT_SECRET,\n  );\n  return token;\n}\n\nexport async function getSession(cookies: Cookies): Promise<Session | null> {\n  const token = cookies.get(COOKIE_NAME);\n  if (!token) return null;\n\n  try {\n    const session = jwt.verify(token, JWT_SECRET) as Session;\n    return session;\n  } catch {\n    return null;\n  }\n}\n\nexport function createSessionCookie(token: string, secure: boolean = true): string {\n  const secureFlag = secure ? \" Secure;\" : \"\";\n  return `${COOKIE_NAME}=${token}; Path=/; HttpOnly; SameSite=Strict;${secureFlag} Max-Age=${SESSION_MAX_AGE}`;\n}\n\nexport function clearSessionCookie(): string {\n  return `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;\n}\n\nexport function createCsrfToken(): string {\n  return crypto.randomBytes(32).toString(\"hex\");\n}\n\nexport function createCsrfCookie(token: string, secure: boolean = true): string {\n  const secureFlag = secure ? \" Secure;\" : \"\";\n  return `${CSRF_COOKIE_NAME}=${token}; Path=/; SameSite=Strict;${secureFlag} Max-Age=${SESSION_MAX_AGE}`;\n}\n\nexport function clearCsrfCookie(): string {\n  return `${CSRF_COOKIE_NAME}=; Path=/; SameSite=Strict; Max-Age=0`;\n}\n"
  },
  {
    "path": "src/lib/server/prisma.ts",
    "content": "import { PrismaClient } from \"@prisma/client\";\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma = globalForPrisma.prisma ?? new PrismaClient();\n\nif (process.env.NODE_ENV !== \"production\") {\n  globalForPrisma.prisma = prisma;\n}\n"
  },
  {
    "path": "src/lib/server/services/auth.ts",
    "content": "import { JWT_SECRET } from \"$env/static/private\";\nimport jsonwebtoken from \"jsonwebtoken\";\nimport bcrypt from \"bcryptjs\";\nimport { prisma } from \"$lib/server/prisma\";\nimport type {\n  UserSession,\n  TokenPayload,\n  AuthServiceInterface,\n} from \"$lib/types/auth\";\n\ninterface UserData {\n  id: number;\n  username: string;\n  email: string | null;\n  isAdmin: boolean;\n}\n\nfunction toUserSession(data: UserData): UserSession {\n  return {\n    id: data.id,\n    username: data.username,\n    email: data.email,\n    isAdmin: Boolean(data.isAdmin),\n  };\n}\n\nclass AuthService implements AuthServiceInterface {\n  private static instance: AuthService;\n\n  private constructor() {}\n\n  static getInstance(): AuthService {\n    if (!AuthService.instance) {\n      AuthService.instance = new AuthService();\n    }\n    return AuthService.instance;\n  }\n\n  async hashPassword(password: string): Promise<string> {\n    const salt = await bcrypt.genSalt(10);\n    return bcrypt.hash(password, salt);\n  }\n\n  async comparePasswords(password: string, hash: string): Promise<boolean> {\n    return bcrypt.compare(password, hash);\n  }\n\n  async generateToken(user: UserSession): Promise<string> {\n    // Only store userId in token - fetch other data from DB when needed\n    // This prevents privilege escalation if JWT secret is compromised\n    const payload: TokenPayload = {\n      userId: user.id,\n    };\n\n    return jsonwebtoken.sign(payload, JWT_SECRET, { expiresIn: \"7d\" });\n  }\n\n  async verifyToken(token: string): Promise<TokenPayload> {\n    try {\n      const decoded = jsonwebtoken.verify(token, JWT_SECRET) as TokenPayload;\n      return decoded;\n    } catch (error) {\n      throw new Error(\"Invalid token\");\n    }\n  }\n\n  async createUser(\n    username: string,\n    email: string | null,\n    password: string,\n  ): Promise<UserSession> {\n    const hashedPassword = await this.hashPassword(password);\n\n    const user = await prisma.user.create({\n      data: {\n        username,\n        email,\n        passwordHash: hashedPassword,\n        isAdmin: false,\n      },\n      select: {\n        id: true,\n        username: true,\n        email: true,\n        isAdmin: true,\n      },\n    });\n\n    return toUserSession(user);\n  }\n\n  async validateUser(\n    usernameOrEmail: string,\n    password: string,\n  ): Promise<UserSession | null> {\n    // Use Prisma query builder instead of raw SQL for better portability\n    const user = await prisma.user.findFirst({\n      where: {\n        OR: [\n          { username: usernameOrEmail },\n          { email: usernameOrEmail },\n        ],\n      },\n      select: {\n        id: true,\n        username: true,\n        email: true,\n        passwordHash: true,\n        isAdmin: true,\n      },\n    });\n\n    if (!user) return null;\n\n    const isValid = await this.comparePasswords(password, user.passwordHash);\n    if (!isValid) return null;\n\n    return toUserSession(user);\n  }\n\n  async findUserByIdentifier(identifier: string): Promise<UserSession | null> {\n    // Use Prisma query builder instead of raw SQL for better portability\n    const user = await prisma.user.findFirst({\n      where: {\n        OR: [\n          { username: identifier },\n          { email: identifier },\n        ],\n      },\n      select: {\n        id: true,\n        username: true,\n        email: true,\n        isAdmin: true,\n      },\n    });\n\n    return user ? toUserSession(user) : null;\n  }\n\n  async updatePassword(userId: number, newPassword: string): Promise<void> {\n    const hashedPassword = await this.hashPassword(newPassword);\n\n    await prisma.user.update({\n      where: { id: userId },\n      data: { passwordHash: hashedPassword },\n    });\n  }\n\n  async createResetToken(identifier: string): Promise<string | null> {\n    const user = await this.findUserByIdentifier(identifier);\n    if (!user) return null;\n\n    const resetToken = jsonwebtoken.sign({ userId: user.id }, JWT_SECRET, {\n      expiresIn: \"1h\",\n    });\n    const resetTokenExp = new Date(Date.now() + 60 * 60 * 1000);\n\n    await prisma.user.update({\n      where: { id: user.id },\n      data: {\n        resetToken,\n        resetTokenExp,\n      },\n    });\n\n    return resetToken;\n  }\n\n  async validateResetToken(token: string): Promise<number | null> {\n    try {\n      const decoded = jsonwebtoken.verify(token, JWT_SECRET) as { userId: number };\n\n      const user = await prisma.user.findFirst({\n        where: {\n          id: decoded.userId,\n          resetToken: token,\n          resetTokenExp: {\n            gt: new Date(),\n          },\n        },\n        select: {\n          id: true,\n        },\n      });\n\n      return user ? user.id : null;\n    } catch {\n      return null;\n    }\n  }\n\n  async clearResetToken(userId: number): Promise<void> {\n    await prisma.user.update({\n      where: { id: userId },\n      data: {\n        resetToken: null,\n        resetTokenExp: null,\n      },\n    });\n  }\n}\n\nexport const authService = AuthService.getInstance();\n"
  },
  {
    "path": "src/lib/server/services/captcha.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { CaptchaService } from \"./captcha\";\n\ndescribe(\"CaptchaService\", () => {\n  it(\"generates and validates captchas\", () => {\n    const { id, text } = CaptchaService.generateCaptcha();\n\n    expect(id).toHaveLength(32);\n    expect(text).toHaveLength(6);\n    expect(CaptchaService.validateCaptcha(id, text, { consume: true })).toBe(true);\n    expect(CaptchaService.validateCaptcha(id, text, { consume: true })).toBe(false);\n  });\n\n  it(\"allows non-consuming validation\", () => {\n    const { id, text } = CaptchaService.generateCaptcha();\n\n    expect(CaptchaService.validateCaptcha(id, text, { consume: false })).toBe(true);\n    expect(CaptchaService.validateCaptcha(id, text, { consume: true })).toBe(true);\n    expect(CaptchaService.validateCaptcha(id, text, { consume: true })).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/lib/server/services/captcha.ts",
    "content": "import crypto from 'crypto';\n\ninterface CaptchaEntry {\n  text: string;\n  createdAt: number;\n}\n\nconst CAPTCHA_EXPIRY = 5 * 60 * 1000; // 5 minutes\nconst captchaStore = new Map<string, CaptchaEntry>();\n\n// Cleanup expired captchas every minute\nsetInterval(() => {\n  const now = Date.now();\n  for (const [id, entry] of captchaStore.entries()) {\n    if (now - entry.createdAt > CAPTCHA_EXPIRY) {\n      captchaStore.delete(id);\n    }\n  }\n}, 60 * 1000);\n\nexport class CaptchaService {\n  private static readonly CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz';\n  private static readonly LENGTH = 6;\n\n  static generateCaptcha(): { id: string; text: string } {\n    const text = Array(this.LENGTH)\n      .fill(0)\n      .map(() => this.CHARS[Math.floor(Math.random() * this.CHARS.length)])\n      .join('');\n\n    const id = crypto.randomBytes(16).toString('hex');\n\n    captchaStore.set(id, {\n      text,\n      createdAt: Date.now(),\n    });\n\n    return { id, text };\n  }\n\n  static validateCaptcha(\n    id: string,\n    userInput: string,\n    options: { consume?: boolean } = {},\n  ): boolean {\n    const entry = captchaStore.get(id);\n\n    if (!entry) {\n      return false;\n    }\n\n    // Check if expired\n    if (Date.now() - entry.createdAt > CAPTCHA_EXPIRY) {\n      captchaStore.delete(id);\n      return false;\n    }\n\n    const matches = entry.text.toLowerCase() === userInput.toLowerCase();\n    const consume = options.consume ?? true;\n    if (consume || !matches) {\n      captchaStore.delete(id);\n    }\n\n    return matches;\n  }\n\n  static invalidateCaptcha(id: string): void {\n    captchaStore.delete(id);\n  }\n}\n"
  },
  {
    "path": "src/lib/server/services/comments.ts",
    "content": "import { prisma } from \"$lib/server/prisma\";\n\ntype MediaType = \"movie\" | \"tv\";\n\ninterface CreateCommentInput {\n  userId: number;\n  mediaId: number;\n  mediaType: MediaType;\n  content: string;\n  parentId?: number | null;\n}\n\ninterface CommentUser {\n  username: string;\n}\n\ninterface CommentCounts {\n  likes: number;\n  replies: number;\n}\n\ninterface BaseComment {\n  id: number;\n  userId: number;\n  mediaId: number;\n  mediaType: string;\n  content: string;\n  createdAt: Date;\n  updatedAt: Date;\n  parentId: number | null;\n  flagged: boolean;\n  flagReason?: string | null;\n  flaggedAt?: Date | null;\n}\n\nexport interface CommentWithDetails extends BaseComment {\n  user: CommentUser;\n  _count: CommentCounts;\n  isLiked?: boolean;\n}\n\nexport class CommentService {\n  private static instance: CommentService;\n\n  private constructor() {}\n\n  static getInstance(): CommentService {\n    if (!CommentService.instance) {\n      CommentService.instance = new CommentService();\n    }\n    return CommentService.instance;\n  }\n\n  async createComment(input: CreateCommentInput): Promise<BaseComment> {\n    return prisma.comment.create({\n      data: {\n        userId: input.userId,\n        mediaId: input.mediaId,\n        mediaType: input.mediaType,\n        content: input.content,\n        parentId: input.parentId,\n      },\n    });\n  }\n\n  async getComments(\n    mediaId: number,\n    mediaType: MediaType,\n    currentUserId?: number | null,\n    parentId: number | null = null,\n    page = 1,\n    limit = 10,\n  ): Promise<{ comments: CommentWithDetails[]; total: number }> {\n    const skip = (page - 1) * limit;\n\n    const [comments, total] = await Promise.all([\n      prisma.comment.findMany({\n        where: {\n          mediaId,\n          mediaType,\n          parentId,\n        },\n        include: {\n          user: {\n            select: {\n              username: true,\n            },\n          },\n          _count: {\n            select: {\n              likes: true,\n              replies: true,\n            },\n          },\n        },\n        orderBy: {\n          createdAt: 'desc',\n        },\n        skip,\n        take: limit,\n      }),\n      prisma.comment.count({\n        where: {\n          mediaId,\n          mediaType,\n          parentId,\n        },\n      }),\n    ]);\n\n    if (currentUserId) {\n      const likes = await prisma.commentLike.findMany({\n        where: {\n          userId: currentUserId,\n          commentId: {\n            in: comments.map(c => c.id),\n          },\n        },\n        select: {\n          commentId: true,\n        },\n      });\n\n      const likedCommentIds = new Set(likes.map(l => l.commentId));\n      comments.forEach(comment => {\n        (comment as CommentWithDetails).isLiked = likedCommentIds.has(comment.id);\n      });\n    }\n\n    return {\n      comments: comments as CommentWithDetails[],\n      total,\n    };\n  }\n\n  async getFlaggedComments(\n    page = 1,\n    limit = 10,\n  ): Promise<{ comments: CommentWithDetails[]; total: number }> {\n    const skip = (page - 1) * limit;\n\n    const [comments, total] = await Promise.all([\n      prisma.comment.findMany({\n        where: {\n          flagged: true,\n        },\n        include: {\n          user: {\n            select: {\n              username: true,\n            },\n          },\n          _count: {\n            select: {\n              likes: true,\n              replies: true,\n            },\n          },\n        },\n        orderBy: {\n          flaggedAt: 'desc',\n        },\n        skip,\n        take: limit,\n      }),\n      prisma.comment.count({\n        where: {\n          flagged: true,\n        },\n      }),\n    ]);\n\n    return {\n      comments: comments as CommentWithDetails[],\n      total,\n    };\n  }\n\n  async getReplies(\n    commentId: number,\n    currentUserId?: number | null,\n    page = 1,\n    limit = 5,\n  ): Promise<{ replies: CommentWithDetails[]; total: number }> {\n    const skip = (page - 1) * limit;\n\n    const [replies, total] = await Promise.all([\n      prisma.comment.findMany({\n        where: {\n          parentId: commentId,\n        },\n        include: {\n          user: {\n            select: {\n              username: true,\n            },\n          },\n          _count: {\n            select: {\n              likes: true,\n              replies: true,\n            },\n          },\n        },\n        orderBy: {\n          createdAt: 'desc',\n        },\n        skip,\n        take: limit,\n      }),\n      prisma.comment.count({\n        where: {\n          parentId: commentId,\n        },\n      }),\n    ]);\n\n    if (currentUserId) {\n      const likes = await prisma.commentLike.findMany({\n        where: {\n          userId: currentUserId,\n          commentId: {\n            in: replies.map(r => r.id),\n          },\n        },\n        select: {\n          commentId: true,\n        },\n      });\n\n      const likedReplyIds = new Set(likes.map(l => l.commentId));\n      replies.forEach(reply => {\n        (reply as CommentWithDetails).isLiked = likedReplyIds.has(reply.id);\n      });\n    }\n\n    return {\n      replies: replies as CommentWithDetails[],\n      total,\n    };\n  }\n\n  async likeComment(userId: number, commentId: number): Promise<void> {\n    await prisma.commentLike.create({\n      data: {\n        userId,\n        commentId,\n      },\n    });\n  }\n\n  async unlikeComment(userId: number, commentId: number): Promise<void> {\n    await prisma.commentLike.delete({\n      where: {\n        userId_commentId: {\n          userId,\n          commentId,\n        },\n      },\n    });\n  }\n\n  async updateComment(\n    commentId: number,\n    userId: number,\n    content: string,\n  ): Promise<BaseComment> {\n    return prisma.comment.update({\n      where: {\n        id: commentId,\n        userId,\n      },\n      data: {\n        content,\n      },\n    });\n  }\n\n  async deleteComment(commentId: number, userId: number): Promise<void> {\n    const comment = await prisma.comment.findUnique({\n      where: { id: commentId },\n    });\n\n    if (!comment) {\n      throw new Error(\"Comment not found\");\n    }\n\n    const user = await prisma.user.findUnique({\n      where: { id: userId },\n      select: { isAdmin: true }\n    });\n\n    if (comment.userId !== userId && !user?.isAdmin) {\n      throw new Error(\"Unauthorized\");\n    }\n\n    await prisma.comment.delete({\n      where: { id: commentId },\n    });\n  }\n\n  async flagComment(commentId: number, reason?: string): Promise<BaseComment> {\n    return prisma.comment.update({\n      where: {\n        id: commentId,\n      },\n      data: {\n        flagged: true,\n        flagReason: reason || \"No reason provided\",\n        flaggedAt: new Date(),\n      },\n    });\n  }\n\n  async unflagComment(commentId: number): Promise<BaseComment> {\n    return prisma.comment.update({\n      where: {\n        id: commentId,\n      },\n      data: {\n        flagged: false,\n        flagReason: null,\n        flaggedAt: null,\n      },\n    });\n  }\n}\n\nexport const commentService = CommentService.getInstance();\n"
  },
  {
    "path": "src/lib/server/services/db-error.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport {\n  PrismaClientInitializationError,\n  PrismaClientKnownRequestError,\n  PrismaClientValidationError,\n} from \"@prisma/client/runtime/library\";\n\nexport interface DbErrorResponse {\n  error: string;\n  code?: string;\n}\n\nexport function isDatabaseConnectionError(error: unknown): boolean {\n  if (error instanceof PrismaClientInitializationError) {\n    return true;\n  }\n  if (error instanceof PrismaClientKnownRequestError) {\n    // P1001: Can't reach database server\n    // P1002: Database server timed out\n    // P1003: Database does not exist\n    // P1008: Operations timed out\n    // P1017: Server has closed the connection\n    const connectionErrors = ['P1001', 'P1002', 'P1003', 'P1008', 'P1017'];\n    return connectionErrors.includes(error.code);\n  }\n  return false;\n}\n\nexport function handleDatabaseError(error: unknown, operation: string) {\n  if (isDatabaseConnectionError(error)) {\n    console.error(`Database unavailable during ${operation}:`,\n      error instanceof Error ? error.message : 'Unknown error'\n    );\n    return json(\n      { error: \"Service temporarily unavailable\", code: \"DB_UNAVAILABLE\" } as DbErrorResponse,\n      { status: 503 }\n    );\n  }\n\n  if (error instanceof PrismaClientKnownRequestError) {\n    console.error(`Database error during ${operation}:`, error.code, error.message);\n\n    // Handle specific known errors\n    switch (error.code) {\n      case 'P2002': // Unique constraint violation\n        return json(\n          { error: \"Resource already exists\", code: \"DUPLICATE\" } as DbErrorResponse,\n          { status: 409 }\n        );\n      case 'P2025': // Record not found\n        return json(\n          { error: \"Resource not found\", code: \"NOT_FOUND\" } as DbErrorResponse,\n          { status: 404 }\n        );\n      default:\n        return json(\n          { error: `Failed to ${operation}`, code: \"DB_ERROR\" } as DbErrorResponse,\n          { status: 500 }\n        );\n    }\n  }\n\n  if (error instanceof PrismaClientValidationError) {\n    console.error(`Validation error during ${operation}:`, error.message);\n    return json(\n      { error: \"Invalid request data\", code: \"VALIDATION_ERROR\" } as DbErrorResponse,\n      { status: 400 }\n    );\n  }\n\n  // Generic error\n  console.error(`Error during ${operation}:`, error);\n  return json(\n    { error: `Failed to ${operation}` } as DbErrorResponse,\n    { status: 500 }\n  );\n}\n"
  },
  {
    "path": "src/lib/server/services/rate-limit.ts",
    "content": "interface RateLimit {\n  count: number;\n  firstAttempt: number;\n}\n\nexport class RateLimitService {\n  private static readonly LOGIN_LIMIT = 5;\n  private static readonly REGISTER_LIMIT = 3;\n  private static readonly COMMENT_LIMIT = 5;\n  private static readonly RESET_PASSWORD_LIMIT = 3;\n  private static readonly LIKE_LIMIT = 30;\n\n  private static readonly LOGIN_WINDOW = 15 * 60 * 1000; // 15 minutes\n  private static readonly REGISTER_WINDOW = 60 * 60 * 1000; // 1 hour\n  private static readonly COMMENT_WINDOW = 5 * 60 * 1000; // 5 minutes\n  private static readonly RESET_PASSWORD_WINDOW = 60 * 60 * 1000; // 1 hour\n  private static readonly LIKE_WINDOW = 60 * 1000; // 1 minute\n\n  private static rateLimits = new Map<string, RateLimit>();\n\n  static checkLoginLimit(ip: string): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `login:${ip}`,\n      this.LOGIN_LIMIT,\n      this.LOGIN_WINDOW\n    );\n  }\n\n  static checkRegisterLimit(ip: string): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `register:${ip}`,\n      this.REGISTER_LIMIT,\n      this.REGISTER_WINDOW\n    );\n  }\n\n  static checkCommentLimit(userId: number): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `comment:${userId}`,\n      this.COMMENT_LIMIT,\n      this.COMMENT_WINDOW\n    );\n  }\n\n  static checkPasswordResetLimit(identifier: string): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `reset:${identifier}`,\n      this.RESET_PASSWORD_LIMIT,\n      this.RESET_PASSWORD_WINDOW\n    );\n  }\n\n  static checkLikeLimit(userId: number): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `like:${userId}`,\n      this.LIKE_LIMIT,\n      this.LIKE_WINDOW\n    );\n  }\n\n  private static checkLimit(\n    key: string,\n    maxAttempts: number,\n    timeWindow: number\n  ): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    const now = Date.now();\n    const limit = this.rateLimits.get(key);\n\n    if (!limit) {\n      this.rateLimits.set(key, { count: 1, firstAttempt: now });\n      return { allowed: true };\n    }\n\n    if (now - limit.firstAttempt >= timeWindow) {\n      this.rateLimits.set(key, { count: 1, firstAttempt: now });\n      return { allowed: true };\n    }\n\n    if (limit.count >= maxAttempts) {\n      const timeLeft = Math.ceil(\n        (timeWindow - (now - limit.firstAttempt)) / 1000\n      );\n      return { allowed: false, timeLeft };\n    }\n\n    limit.count++;\n    this.rateLimits.set(key, limit);\n    return { allowed: true };\n  }\n\n  static cleanup() {\n    const now = Date.now();\n    const maxWindow = Math.max(\n      this.LOGIN_WINDOW,\n      this.REGISTER_WINDOW,\n      this.COMMENT_WINDOW,\n      this.RESET_PASSWORD_WINDOW\n    );\n\n    for (const [key, limit] of this.rateLimits.entries()) {\n      if (now - limit.firstAttempt >= maxWindow) {\n        this.rateLimits.delete(key);\n      }\n    }\n  }\n}\n\nsetInterval(() => RateLimitService.cleanup(), 60 * 1000);\n\n// Legacy exports for backward compatibility\nclass InstanceRateLimitService {\n  private requestCounts = new Map<string, { count: number; timestamp: number }>();\n  private readonly windowMs: number;\n  private readonly maxRequests: number;\n\n  constructor(windowMs: number = 60 * 1000, maxRequests: number = 100) {\n    this.windowMs = windowMs;\n    this.maxRequests = maxRequests;\n  }\n\n  checkRateLimit(ip: string): boolean {\n    const now = Date.now();\n    const userRequests = this.requestCounts.get(ip);\n\n    if (!userRequests) {\n      this.requestCounts.set(ip, { count: 1, timestamp: now });\n      return true;\n    }\n\n    if (now - userRequests.timestamp > this.windowMs) {\n      this.requestCounts.set(ip, { count: 1, timestamp: now });\n      return true;\n    }\n\n    if (userRequests.count >= this.maxRequests) {\n      return false;\n    }\n\n    userRequests.count++;\n    return true;\n  }\n}\n\nexport const commentRateLimit = new InstanceRateLimitService(60 * 1000, 100);\nexport const authRateLimit = new InstanceRateLimitService(15 * 60 * 1000, 50);\n"
  },
  {
    "path": "src/lib/server/services/watchlist.ts",
    "content": "import { prisma } from \"$lib/server/prisma\";\n\ninterface PrismaError extends Error {\n  code?: string;\n}\n\nexport class WatchlistService {\n  private static instance: WatchlistService;\n\n  private constructor() {}\n\n  static getInstance(): WatchlistService {\n    if (!WatchlistService.instance) {\n      WatchlistService.instance = new WatchlistService();\n    }\n    return WatchlistService.instance;\n  }\n\n  async addToWatchlist(\n    userId: number,\n    mediaId: number,\n    mediaType: \"movie\" | \"tv\",\n    title: string,\n    posterPath: string | null,\n    voteAverage: number,\n  ) {\n    try {\n      const watchlistItem = await prisma.watchlist.create({\n        data: {\n          userId,\n          mediaId,\n          mediaType,\n          title,\n          posterPath,\n          voteAverage,\n        },\n      });\n      return watchlistItem;\n    } catch (error) {\n      if ((error as PrismaError).code === \"P2002\") {\n        throw new Error(\"Item already in watchlist\");\n      }\n      throw error;\n    }\n  }\n\n  async removeFromWatchlist(\n    userId: number,\n    mediaId: number,\n    mediaType: \"movie\" | \"tv\",\n  ) {\n    return prisma.watchlist.deleteMany({\n      where: {\n        userId,\n        mediaId,\n        mediaType,\n      },\n    });\n  }\n\n  async getWatchlist(userId: number) {\n    return prisma.watchlist.findMany({\n      where: {\n        userId,\n      },\n      orderBy: {\n        addedAt: \"desc\",\n      },\n    });\n  }\n\n  async isInWatchlist(\n    userId: number,\n    mediaId: number,\n    mediaType: \"movie\" | \"tv\",\n  ) {\n    const count = await prisma.watchlist.count({\n      where: {\n        userId,\n        mediaId,\n        mediaType,\n      },\n    });\n    return count > 0;\n  }\n\n  async getWatchlistCount(userId: number) {\n    return prisma.watchlist.count({\n      where: {\n        userId,\n      },\n    });\n  }\n}\n\nexport const watchlistService = WatchlistService.getInstance();\n"
  },
  {
    "path": "src/lib/services/api-client.ts",
    "content": "class ApiError extends Error {\n  constructor(\n    public status: number,\n    message: string,\n  ) {\n    super(message);\n    this.name = \"ApiError\";\n  }\n}\n\nexport class ApiClient {\n  constructor(\n    private baseUrl: string,\n    private apiKey?: string,\n  ) {}\n\n  private async handleResponse<T>(response: Response): Promise<T> {\n    if (!response.ok) {\n      throw new ApiError(\n        response.status,\n        `API request failed: ${response.statusText}`,\n      );\n    }\n\n    const data = await response.json();\n    return data as T;\n  }\n\n  async get<T>(\n    endpoint: string,\n    params: Record<string, string> = {},\n  ): Promise<T> {\n    const searchParams = new URLSearchParams(params);\n    if (this.apiKey) {\n      searchParams.append(\"api_key\", this.apiKey);\n    }\n\n    const url = `${this.baseUrl}${endpoint}?${searchParams.toString()}`;\n    const response = await fetch(url);\n    return this.handleResponse<T>(response);\n  }\n}\n"
  },
  {
    "path": "src/lib/services/auth.ts",
    "content": "import type { UserSession } from \"$lib/types/auth\";\nimport { csrfFetch } from \"$lib/utils/csrf\";\n\nclass AuthService {\n  private static instance: AuthService;\n\n  private constructor() {}\n\n  static getInstance(): AuthService {\n    if (!AuthService.instance) {\n      AuthService.instance = new AuthService();\n    }\n    return AuthService.instance;\n  }\n\n  async login(usernameOrEmail: string, password: string): Promise<UserSession> {\n    const response = await csrfFetch('/api/auth/login', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ usernameOrEmail, password }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to login');\n    }\n\n    return response.json();\n  }\n\n  async register(\n    username: string,\n    email: string | null,\n    password: string,\n    captchaId: string,\n    captchaAnswer: string,\n  ): Promise<UserSession> {\n    const response = await csrfFetch('/api/auth/register', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ username, email, password, captchaId, captchaAnswer }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to register');\n    }\n\n    return response.json();\n  }\n\n  async logout(): Promise<void> {\n    const response = await csrfFetch('/api/auth/logout', {\n      method: 'POST',\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to logout');\n    }\n  }\n\n  async getCurrentUser(): Promise<UserSession | null> {\n    const response = await fetch('/api/auth/me');\n\n    if (!response.ok) {\n      if (response.status === 401) {\n        return null;\n      }\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to get current user');\n    }\n\n    return response.json();\n  }\n\n  async requestPasswordReset(identifier: string): Promise<void> {\n    const response = await csrfFetch('/api/auth/reset-password/request', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ identifier }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to request password reset');\n    }\n  }\n\n  async resetPassword(token: string, newPassword: string): Promise<void> {\n    const response = await csrfFetch('/api/auth/reset-password/reset', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ token, newPassword }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to reset password');\n    }\n  }\n}\n\nexport const authService = AuthService.getInstance();\n"
  },
  {
    "path": "src/lib/services/captcha.ts",
    "content": "export class CaptchaService {\n  private static readonly OPERATORS = [\"+\", \"-\", \"*\"] as const;\n  private static readonly MAX_NUMBER = 10;\n  private static readonly MIN_NUMBER = 1;\n\n  static generateChallenge(): { question: string; answer: number } {\n    const num1 =\n      Math.floor(Math.random() * (this.MAX_NUMBER - this.MIN_NUMBER + 1)) +\n      this.MIN_NUMBER;\n    const num2 =\n      Math.floor(Math.random() * (this.MAX_NUMBER - this.MIN_NUMBER + 1)) +\n      this.MIN_NUMBER;\n    const operator =\n      this.OPERATORS[Math.floor(Math.random() * this.OPERATORS.length)];\n\n    let answer: number;\n    switch (operator) {\n      case \"+\":\n        answer = num1 + num2;\n        break;\n      case \"-\":\n        answer = num1 - num2;\n        break;\n      case \"*\":\n        answer = num1 * num2;\n        break;\n    }\n\n    const question = `What is ${num1} ${operator} ${num2}?`;\n    return { question, answer };\n  }\n\n  static validateAnswer(\n    userAnswer: string | number,\n    correctAnswer: number,\n  ): boolean {\n    const parsedAnswer =\n      typeof userAnswer === \"string\" ? parseInt(userAnswer, 10) : userAnswer;\n    return !isNaN(parsedAnswer) && parsedAnswer === correctAnswer;\n  }\n}\n"
  },
  {
    "path": "src/lib/services/comments.ts",
    "content": "import { csrfFetch } from \"$lib/utils/csrf\";\n\ntype MediaType = \"movie\" | \"tv\";\n\ninterface CommentUser {\n  username: string;\n}\n\ninterface CommentCounts {\n  likes: number;\n  replies: number;\n}\n\ninterface BaseComment {\n  id: number;\n  userId: number;\n  mediaId: number;\n  mediaType: string;\n  content: string;\n  createdAt: Date;\n  updatedAt: Date;\n  parentId: number | null;\n  flagged: boolean;\n  flagReason?: string | null;\n  flaggedAt?: Date | null;\n}\n\nexport interface CommentWithDetails extends BaseComment {\n  user: CommentUser;\n  _count: CommentCounts;\n  isLiked?: boolean;\n}\n\nexport class CommentService {\n  async createComment(\n    mediaId: number,\n    mediaType: MediaType,\n    content: string,\n    parentId?: number | null,\n  ): Promise<BaseComment> {\n    const response = await csrfFetch('/api/comments', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        mediaId,\n        mediaType,\n        content,\n        parentId,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to create comment');\n    }\n\n    return response.json();\n  }\n\n  async getComments(\n    mediaId: number,\n    mediaType: MediaType,\n    parentId: number | null = null,\n    page = 1,\n    limit = 10,\n  ): Promise<{ comments: CommentWithDetails[]; total: number }> {\n    const params = new URLSearchParams({\n      mediaId: mediaId.toString(),\n      mediaType,\n      page: page.toString(),\n      limit: limit.toString(),\n    });\n\n    if (parentId !== null) {\n      params.append('parentId', parentId.toString());\n    }\n\n    const response = await fetch(`/api/comments?${params}`);\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to fetch comments');\n    }\n\n    return response.json();\n  }\n\n  async getFlaggedComments(\n    page = 1,\n    limit = 10,\n  ): Promise<{ comments: CommentWithDetails[]; total: number }> {\n    const params = new URLSearchParams({\n      page: page.toString(),\n      limit: limit.toString(),\n    });\n\n    const response = await fetch(`/api/comments/flagged?${params}`);\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to fetch flagged comments');\n    }\n\n    return response.json();\n  }\n\n  async getReplies(\n    commentId: number,\n    page = 1,\n    limit = 5,\n  ): Promise<{ replies: CommentWithDetails[]; total: number }> {\n    const params = new URLSearchParams({\n      page: page.toString(),\n      limit: limit.toString(),\n    });\n\n    const response = await fetch(`/api/comments/${commentId}?${params}`);\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to fetch replies');\n    }\n\n    return response.json();\n  }\n\n  async likeComment(commentId: number): Promise<void> {\n    const response = await csrfFetch('/api/comments/like', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ commentId }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to like comment');\n    }\n  }\n\n  async updateComment(\n    commentId: number,\n    content: string,\n  ): Promise<BaseComment> {\n    const response = await csrfFetch(`/api/comments/${commentId}`, {\n      method: 'PUT',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ content }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to update comment');\n    }\n\n    return response.json();\n  }\n\n  async deleteComment(commentId: number): Promise<void> {\n    const response = await csrfFetch(`/api/comments/${commentId}`, {\n      method: 'DELETE',\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to delete comment');\n    }\n  }\n\n  async flagComment(commentId: number, reason?: string): Promise<void> {\n    const response = await csrfFetch(`/api/comments/${commentId}/flag`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ reason }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to flag comment');\n    }\n  }\n\n  async unflagComment(commentId: number): Promise<void> {\n    const response = await csrfFetch(`/api/comments/${commentId}/unflag`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to unflag comment');\n    }\n  }\n}\n\nexport const commentService = new CommentService();\n"
  },
  {
    "path": "src/lib/services/image.ts",
    "content": "import sharp from \"sharp\";\nimport { createHash } from \"crypto\";\nimport { mkdir, access, writeFile, readdir, unlink } from \"fs/promises\";\nimport { join } from \"path\";\nimport { PrismaClient } from \"@prisma/client\";\n\nconst prisma = new PrismaClient();\n\ninterface ImageOptions {\n  width?: number;\n  height?: number;\n  quality?: number;\n  format?: \"jpeg\" | \"webp\" | \"avif\" | \"png\";\n}\n\ninterface CacheEntry {\n  id: number;\n  url: string;\n  path: string;\n  format: string;\n  width: number | null;\n  height: number | null;\n  quality: number;\n  createdAt: Date;\n  accessedAt: Date;\n}\n\nconst CACHE_DIR = \"static/image-cache\";\nconst DEFAULT_QUALITY = 80;\nconst DEFAULT_FORMAT = \"webp\";\nconst CACHE_CLEANUP_DAYS = 7;\n\nexport class ImageService {\n  private static instance: ImageService;\n\n  private constructor() {\n    this.ensureCacheDir();\n    this.scheduleCleanup();\n  }\n\n  static getInstance(): ImageService {\n    if (!ImageService.instance) {\n      ImageService.instance = new ImageService();\n    }\n    return ImageService.instance;\n  }\n\n  private async ensureCacheDir() {\n    try {\n      await access(CACHE_DIR);\n    } catch {\n      await mkdir(CACHE_DIR, { recursive: true });\n    }\n  }\n\n  private generateCacheKey(url: string, options: ImageOptions): string {\n    const hash = createHash(\"md5\");\n    hash.update(url + JSON.stringify(options));\n    return hash.digest(\"hex\");\n  }\n\n  private getCachePath(key: string, format: string): string {\n    return join(CACHE_DIR, `${key}.${format}`);\n  }\n\n  private scheduleCleanup() {\n\n    setInterval(() => this.cleanupCache(), 24 * 60 * 60 * 1000);\n  }\n\n  async cleanupCache(): Promise<void> {\n    try {\n      const cutoffDate = new Date();\n      cutoffDate.setDate(cutoffDate.getDate() - CACHE_CLEANUP_DAYS);\n\n\n      const oldEntries = await prisma.$queryRaw<CacheEntry[]>`\n        SELECT * FROM image_cache\n        WHERE accessedAt < ${cutoffDate}\n      `;\n\n\n      await Promise.all(\n        oldEntries.map(async (entry) => {\n          try {\n            await unlink(join(\"static\", entry.path));\n            await prisma.$executeRaw`\n            DELETE FROM image_cache WHERE id = ${entry.id}\n          `;\n          } catch (error) {\n            console.error(\"Error cleaning up cache entry:\", error);\n          }\n        }),\n      );\n    } catch (error) {\n      console.error(\"Error during cache cleanup:\", error);\n    }\n  }\n\n  async optimizeImage(\n    url: string,\n    options: ImageOptions = {},\n  ): Promise<string> {\n    const {\n      width,\n      height,\n      quality = DEFAULT_QUALITY,\n      format = DEFAULT_FORMAT,\n    } = options;\n\n    const cacheKey = this.generateCacheKey(url, options);\n    const cachePath = this.getCachePath(cacheKey, format);\n    const relativePath = cachePath.replace(\"static\", \"\");\n\n    try {\n\n      const cached = await prisma.$queryRaw<CacheEntry[]>`\n        SELECT * FROM image_cache\n        WHERE url = ${url}\n          AND format = ${format}\n          AND width = ${width || null}\n          AND height = ${height || null}\n          AND quality = ${quality}\n        LIMIT 1\n      `;\n\n      if (cached.length > 0) {\n\n        await prisma.$executeRaw`\n          UPDATE image_cache\n          SET accessedAt = ${new Date()}\n          WHERE id = ${cached[0].id}\n        `;\n        return cached[0].path;\n      }\n\n\n      const response = await fetch(url);\n      const buffer = Buffer.from(await response.arrayBuffer());\n\n      let pipeline = sharp(buffer);\n\n\n      if (width || height) {\n        pipeline = pipeline.resize(width, height, {\n          fit: \"cover\",\n          withoutEnlargement: true,\n        });\n      }\n\n\n      switch (format) {\n        case \"jpeg\":\n          pipeline = pipeline.jpeg({ quality });\n          break;\n        case \"webp\":\n          pipeline = pipeline.webp({ quality });\n          break;\n        case \"avif\":\n          pipeline = pipeline.avif({ quality });\n          break;\n        case \"png\":\n          pipeline = pipeline.png({ quality });\n          break;\n      }\n\n      const optimizedBuffer = await pipeline.toBuffer();\n      await writeFile(cachePath, optimizedBuffer);\n\n\n      await prisma.$executeRaw`\n        INSERT INTO image_cache (url, path, format, width, height, quality, createdAt, accessedAt)\n        VALUES (\n          ${url},\n          ${relativePath},\n          ${format},\n          ${width || null},\n          ${height || null},\n          ${quality},\n          ${new Date()},\n          ${new Date()}\n        )\n      `;\n\n      return relativePath;\n    } catch (error) {\n      console.error(\"Image optimization error:\", error);\n      throw error;\n    }\n  }\n\n  async generateResponsiveSet(\n    url: string,\n    breakpoints: number[] = [320, 640, 768, 1024, 1280],\n  ): Promise<string[]> {\n    const promises = breakpoints.map((width) =>\n      this.optimizeImage(url, { width, format: \"webp\" }),\n    );\n\n    return Promise.all(promises);\n  }\n\n  async generateSrcSet(\n    url: string,\n    breakpoints: number[] = [320, 640, 768, 1024, 1280],\n  ): Promise<string> {\n    const paths = await this.generateResponsiveSet(url, breakpoints);\n    return paths\n      .map((path, index) => `${path} ${breakpoints[index]}w`)\n      .join(\", \");\n  }\n\n\n  isValidUrl(url: string): boolean {\n    try {\n      new URL(url);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n\nexport const imageService = ImageService.getInstance();\n"
  },
  {
    "path": "src/lib/services/providers.ts",
    "content": "import { browser } from \"$app/environment\";\nimport { get } from \"svelte/store\";\nimport { providerUrls } from \"$lib/stores/provider-urls\";\n\nexport interface Provider {\n  id: string;\n  name: string;\n  getEmbedUrl: (\n    mediaId: string | number,\n    type: \"movie\" | \"tv\",\n    season?: number,\n    episode?: number,\n  ) => string;\n}\n\nexport const providers: Provider[] = [\n  {\n    id: \"vidsrc\",\n    name: \"VidSrc\",\n    getEmbedUrl: (mediaId, type, season, episode) => {\n      const urls = get(providerUrls);\n      if (!urls?.vidsrc) return \"\";\n\n      if (type === \"movie\") {\n        return `${urls.vidsrc}/movie/${mediaId}?autoPlay=true`;\n      } else {\n        if (typeof season !== \"undefined\" && typeof episode !== \"undefined\") {\n          return `${urls.vidsrc}/tv/${mediaId}/${season}/${episode}?autoPlay=true&autoNext=true`;\n        }\n        return `${urls.vidsrc}/tv/${mediaId}?autoPlay=true`;\n      }\n    },\n  },\n  {\n    id: \"vidlink\",\n    name: \"VidLink\",\n    getEmbedUrl: (mediaId, type, season, episode) => {\n      const urls = get(providerUrls);\n      if (!urls?.vidlink) return \"\";\n\n      if (type === \"movie\") {\n        return `${urls.vidlink}/movie/${mediaId}?autoplay=true&title=true`;\n      } else {\n        if (typeof season !== \"undefined\" && typeof episode !== \"undefined\") {\n          return `${urls.vidlink}/tv/${mediaId}/${season}/${episode}?autoplay=true&title=true`;\n        }\n        return `${urls.vidlink}/tv/${mediaId}/1/1?autoplay=true&title=true`;\n      }\n    },\n  },\n  {\n    id: \"111movies\",\n    name: \"111Movies\",\n    getEmbedUrl: (mediaId, type, season, episode) => {\n      const urls = get(providerUrls);\n      if (!urls?.movies111) return \"\";\n\n      if (type === \"movie\") {\n        return `${urls.movies111}/movie/${mediaId}`;\n      } else {\n        if (typeof season !== \"undefined\" && typeof episode !== \"undefined\") {\n          return `${urls.movies111}/tv/${mediaId}/${season}/${episode}`;\n        }\n        return `${urls.movies111}/tv/${mediaId}/1/1`;\n      }\n    },\n  },\n  {\n    id: \"2embed\",\n    name: \"2Embed\",\n    getEmbedUrl: (mediaId, type, season, episode) => {\n      const urls = get(providerUrls);\n      if (!urls?.embed2) return \"\";\n\n      if (type === \"movie\") {\n        return `${urls.embed2}/embed/${mediaId}`;\n      } else {\n        if (typeof season !== \"undefined\" && typeof episode !== \"undefined\") {\n          return `${urls.embed2}/embedtv/${mediaId}&s=${season}&e=${episode}`;\n        }\n        return `${urls.embed2}/embedtv/${mediaId}&s=1&e=1`;\n      }\n    },\n  },\n];\n\nexport function getProvider(id: string): Provider | undefined {\n  return providers.find((p) => p.id === id);\n}\n\nexport function getDefaultProvider(): Provider {\n  if (!browser) {\n    return providers[0];\n  }\n\n  const savedProvider = localStorage.getItem(\"selectedProvider\");\n  if (savedProvider) {\n    const provider = providers.find((p) => p.id === savedProvider);\n    if (provider) return provider;\n  }\n\n  return providers.find((p) => p.id === \"vidsrc\") || providers[0];\n}\n"
  },
  {
    "path": "src/lib/services/rate-limit.ts",
    "content": "interface RateLimit {\n  count: number;\n  firstAttempt: number;\n}\n\nexport class RateLimitService {\n  private static readonly COMMENT_LIMIT = 5;\n  private static readonly RESET_PASSWORD_LIMIT = 3;\n  private static readonly COMMENT_WINDOW = 5 * 60 * 1000;\n  private static readonly RESET_PASSWORD_WINDOW = 60 * 60 * 1000;\n  private static rateLimits = new Map<string, RateLimit>();\n\n  static checkCommentLimit(userId: number): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `comment:${userId}`,\n      this.COMMENT_LIMIT,\n      this.COMMENT_WINDOW\n    );\n  }\n\n  static checkPasswordResetLimit(identifier: string): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    return this.checkLimit(\n      `reset:${identifier}`,\n      this.RESET_PASSWORD_LIMIT,\n      this.RESET_PASSWORD_WINDOW\n    );\n  }\n\n  private static checkLimit(\n    key: string,\n    maxAttempts: number,\n    timeWindow: number\n  ): {\n    allowed: boolean;\n    timeLeft?: number;\n  } {\n    const now = Date.now();\n    const limit = this.rateLimits.get(key);\n\n    if (!limit) {\n      this.rateLimits.set(key, { count: 1, firstAttempt: now });\n      return { allowed: true };\n    }\n\n    if (now - limit.firstAttempt >= timeWindow) {\n      this.rateLimits.set(key, { count: 1, firstAttempt: now });\n      return { allowed: true };\n    }\n\n    if (limit.count >= maxAttempts) {\n      const timeLeft = Math.ceil(\n        (timeWindow - (now - limit.firstAttempt)) / 1000\n      );\n      return { allowed: false, timeLeft };\n    }\n\n    limit.count++;\n    this.rateLimits.set(key, limit);\n    return { allowed: true };\n  }\n\n  static cleanup() {\n    const now = Date.now();\n    const maxWindow = Math.max(this.COMMENT_WINDOW, this.RESET_PASSWORD_WINDOW);\n\n    for (const [key, limit] of this.rateLimits.entries()) {\n      if (now - limit.firstAttempt >= maxWindow) {\n        this.rateLimits.delete(key);\n      }\n    }\n  }\n\n  static startCleanup() {\n    setInterval(() => this.cleanup(), Math.max(this.COMMENT_WINDOW, this.RESET_PASSWORD_WINDOW));\n  }\n}\n"
  },
  {
    "path": "src/lib/services/release-type.ts",
    "content": "interface ReleaseInfo {\n  releaseType: string;\n  certifications: Record<string, string>;\n}\n\nconst cache = new Map<string, ReleaseInfo>();\n\nexport async function getReleaseType(\n  mediaId: number,\n  mediaType: string,\n): Promise<ReleaseInfo> {\n  try {\n    const cacheKey = `${mediaId}_${mediaType}`;\n    if (cache.has(cacheKey)) {\n      return cache.get(cacheKey)!;\n    }\n\n    const response = await fetch(`/api/release-info/${mediaType}/${mediaId}`);\n\n    if (!response.ok) {\n      return {\n        releaseType: \"Unknown Quality\",\n        certifications: {},\n      };\n    }\n\n    const result: ReleaseInfo = await response.json();\n    cache.set(cacheKey, result);\n\n    return result;\n  } catch (error) {\n    console.error(\"Error fetching release type and certifications:\", error);\n    return {\n      releaseType: \"Unknown Quality\",\n      certifications: {},\n    };\n  }\n}\n"
  },
  {
    "path": "src/lib/services/tmdb.ts",
    "content": "import { TMDB_API_KEY } from '$env/static/private';\nimport type { TMDBMovie, TMDBTVShow, TMDBResponse, TMDBGenre, TMDBMediaResponse } from '$lib/types/tmdb';\n\nconst TMDB_BASE_URL = 'https://api.themoviedb.org/3';\nconst MAX_RETRIES = 2;\ntype QueryParams = Record<string, string | number | boolean | null | undefined>;\ntype HasVoteAverage = { vote_average?: number | null };\n\nexport class TMDBApiError extends Error {\n  constructor(\n    message: string,\n    public statusCode: number,\n    public isAuthError: boolean = false\n  ) {\n    super(message);\n    this.name = 'TMDBApiError';\n  }\n}\n\nexport class TMDBService {\n  private apiKey: string;\n  private baseUrl: string;\n\n  constructor() {\n    this.apiKey = TMDB_API_KEY || '';\n    this.baseUrl = TMDB_BASE_URL;\n  }\n\n  isConfigured(): boolean {\n    return !!this.apiKey && this.apiKey.length > 0;\n  }\n\n  private async fetch<T>(endpoint: string, params: QueryParams = {}, retryCount = 0): Promise<T> {\n    if (!this.isConfigured()) {\n      throw new TMDBApiError(\n        'TMDB API key is not configured. Please add TMDB_API_KEY to your .env file.',\n        401,\n        true\n      );\n    }\n\n    const url = new URL(`${this.baseUrl}${endpoint}`);\n    url.searchParams.append('api_key', this.apiKey);\n\n    if (!params['vote_average.gte']) {\n      params['vote_average.gte'] = 0.1;\n    }\n\n    for (const [key, value] of Object.entries(params)) {\n      if (value !== undefined && value !== null) {\n        url.searchParams.append(key, value.toString());\n      }\n    }\n\n    try {\n      const response = await fetch(url.toString());\n\n      if (!response.ok) {\n        if (response.status === 401) {\n          throw new TMDBApiError(\n            'Invalid TMDB API key. Please check your TMDB_API_KEY in .env file.',\n            401,\n            true\n          );\n        }\n\n        if (response.status >= 400 && response.status < 500) {\n          throw new TMDBApiError(\n            `TMDB API client error: ${response.status} ${response.statusText}`,\n            response.status\n          );\n        }\n\n        if (response.status >= 500 && retryCount < MAX_RETRIES) {\n          await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));\n          return this.fetch<T>(endpoint, params, retryCount + 1);\n        }\n\n        throw new TMDBApiError(\n          `TMDB API error: ${response.status} ${response.statusText}`,\n          response.status\n        );\n      }\n\n      const data = await response.json() as T;\n      const results = (data as { results?: HasVoteAverage[] }).results;\n\n      if (Array.isArray(results)) {\n        (data as { results: HasVoteAverage[] }).results = results.filter(\n          (item) => (item.vote_average ?? 0) > 0,\n        );\n      }\n\n      return data;\n    } catch (error) {\n      if (error instanceof TMDBApiError) {\n        throw error;\n      }\n      if (error instanceof Error) {\n        throw new TMDBApiError(error.message, 500);\n      }\n      throw new TMDBApiError('Failed to fetch from TMDB API', 500);\n    }\n  }\n\n  async getMovieDetails(id: number): Promise<TMDBMovie> {\n    return this.fetch<TMDBMovie>(`/movie/${id}`, {\n      append_to_response: 'videos'\n    });\n  }\n\n  async getTVShowDetails(id: number): Promise<TMDBTVShow> {\n    return this.fetch<TMDBTVShow>(`/tv/${id}`, {\n      append_to_response: 'videos'\n    });\n  }\n\n  async getMovieGenres(): Promise<TMDBGenre[]> {\n    const response = await this.fetch<{ genres: TMDBGenre[] }>('/genre/movie/list');\n    return response.genres;\n  }\n\n  async getTVGenres(): Promise<TMDBGenre[]> {\n    const response = await this.fetch<{ genres: TMDBGenre[] }>('/genre/tv/list');\n    return response.genres;\n  }\n\n  async searchMovies(query: string, page = 1): Promise<TMDBResponse<TMDBMovie>> {\n    return this.fetch<TMDBResponse<TMDBMovie>>('/search/movie', {\n      query,\n      page,\n      include_adult: false,\n      language: 'en-US'\n    });\n  }\n\n  async searchTVShows(query: string, page = 1): Promise<TMDBResponse<TMDBTVShow>> {\n    return this.fetch<TMDBResponse<TMDBTVShow>>('/search/tv', {\n      query,\n      page,\n      include_adult: false,\n      language: 'en-US'\n    });\n  }\n\n  async searchMulti(query: string, page = 1): Promise<TMDBResponse<TMDBMediaResponse>> {\n    return this.fetch<TMDBResponse<TMDBMediaResponse>>('/search/multi', {\n      query,\n      page,\n      include_adult: false,\n      language: 'en-US'\n    });\n  }\n\n  async discoverMovies(params: QueryParams = {}): Promise<TMDBResponse<TMDBMovie>> {\n    return this.fetch<TMDBResponse<TMDBMovie>>('/discover/movie', {\n      include_adult: false,\n      language: 'en-US',\n      ...params\n    });\n  }\n\n  async discoverTVShows(params: QueryParams = {}): Promise<TMDBResponse<TMDBTVShow>> {\n    return this.fetch<TMDBResponse<TMDBTVShow>>('/discover/tv', {\n      include_adult: false,\n      language: 'en-US',\n      ...params\n    });\n  }\n\n  async getTrending(mediaType: 'movie' | 'tv', timeWindow: 'day' | 'week' = 'week'): Promise<TMDBResponse<TMDBMediaResponse>> {\n    return this.fetch<TMDBResponse<TMDBMediaResponse>>(`/trending/${mediaType}/${timeWindow}`);\n  }\n\n  async getTrendingMovies(timeWindow: 'day' | 'week' = 'week'): Promise<TMDBResponse<TMDBMovie>> {\n    return this.fetch<TMDBResponse<TMDBMovie>>(`/trending/movie/${timeWindow}`);\n  }\n\n  async getTrendingTVShows(timeWindow: 'day' | 'week' = 'week'): Promise<TMDBResponse<TMDBTVShow>> {\n    return this.fetch<TMDBResponse<TMDBTVShow>>(`/trending/tv/${timeWindow}`);\n  }\n\n  async getPopularMovies(page = 1): Promise<TMDBResponse<TMDBMovie>> {\n    return this.fetch<TMDBResponse<TMDBMovie>>('/movie/popular', { page });\n  }\n\n  async getPopularTVShows(page = 1): Promise<TMDBResponse<TMDBTVShow>> {\n    return this.fetch<TMDBResponse<TMDBTVShow>>('/tv/popular', { page });\n  }\n\n  async getTopRatedMovies(page = 1): Promise<TMDBResponse<TMDBMovie>> {\n    return this.fetch<TMDBResponse<TMDBMovie>>('/movie/top_rated', { page });\n  }\n\n  async getTopRatedTVShows(page = 1): Promise<TMDBResponse<TMDBTVShow>> {\n    return this.fetch<TMDBResponse<TMDBTVShow>>('/tv/top_rated', { page });\n  }\n\n  async getUpcomingMovies(page = 1): Promise<TMDBResponse<TMDBMovie>> {\n    return this.fetch<TMDBResponse<TMDBMovie>>('/movie/upcoming', { page });\n  }\n\n  async getOnTheAirTVShows(page = 1): Promise<TMDBResponse<TMDBTVShow>> {\n    return this.fetch<TMDBResponse<TMDBTVShow>>('/tv/on_the_air', { page });\n  }\n\n  getImageUrl(path: string | null, size: 'original' | 'w500' | 'w780' = 'w500'): string | null {\n    if (!path) return null;\n    return `https://image.tmdb.org/t/p/${size}${path}`;\n  }\n}\n"
  },
  {
    "path": "src/lib/services/watchlist.ts",
    "content": "import { csrfFetch } from \"$lib/utils/csrf\";\n\ninterface WatchlistItem {\n  id: number;\n  userId: number;\n  mediaId: number;\n  mediaType: \"movie\" | \"tv\";\n  title: string;\n  posterPath: string | null;\n  voteAverage: number;\n  addedAt: string;\n}\n\nexport class WatchlistService {\n  async addToWatchlist(\n    mediaId: number,\n    mediaType: \"movie\" | \"tv\",\n    title: string,\n    posterPath: string | null,\n    voteAverage: number,\n  ): Promise<WatchlistItem> {\n    const response = await csrfFetch('/api/watchlist', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        mediaId,\n        mediaType,\n        title,\n        posterPath,\n        voteAverage,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to add to watchlist');\n    }\n\n    return response.json();\n  }\n\n  async removeFromWatchlist(\n    mediaId: number,\n    mediaType: \"movie\" | \"tv\",\n  ): Promise<void> {\n    const response = await csrfFetch('/api/watchlist', {\n      method: 'DELETE',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ mediaId, mediaType }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to remove from watchlist');\n    }\n  }\n\n  async getWatchlist(): Promise<WatchlistItem[]> {\n    const response = await fetch('/api/watchlist');\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to fetch watchlist');\n    }\n\n    return response.json();\n  }\n\n  async isInWatchlist(\n    mediaId: number,\n    mediaType: \"movie\" | \"tv\",\n  ): Promise<boolean> {\n    const response = await fetch(`/api/watchlist/check?mediaId=${mediaId}&mediaType=${mediaType}`);\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || 'Failed to check watchlist status');\n    }\n\n    const data = await response.json();\n    return data.inWatchlist;\n  }\n}\n\nexport const watchlistService = new WatchlistService();\n"
  },
  {
    "path": "src/lib/shared/comment-validation.ts",
    "content": "export function containsUrl(text: string): boolean {\n  const urlPatterns = [\n    /https?:\\/\\/[^\\s/$.?#].[^\\s]*/i,\n    /www\\.[^\\s/$.?#].[^\\s]*/i,\n    /[^\\s/$.?#]+\\.(com|net|org|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum|fr|uk|us|ca|eu|de|it|es)[^\\s]/i\n  ];\n\n  return urlPatterns.some(pattern => pattern.test(text));\n}\n\nexport function validateComment(content: string): { isValid: boolean; error?: string } {\n  if (!content || content.trim().length === 0) {\n    return { isValid: false, error: 'Comment cannot be empty' };\n  }\n\n  if (containsUrl(content)) {\n    return { isValid: false, error: 'URLs are not allowed in comments' };\n  }\n\n  return { isValid: true };\n}\n"
  },
  {
    "path": "src/lib/stores/auth.ts",
    "content": "import { writable } from \"svelte/store\";\nimport { csrfFetch } from \"$lib/utils/csrf\";\n\ninterface User {\n  id: number;\n  username: string;\n  email: string | null;\n  isAdmin: boolean;\n}\n\ninterface AuthState {\n  isAuthenticated: boolean;\n  user: User | null;\n  loading: boolean;\n  error: string | null;\n}\n\nfunction createAuthStore() {\n  const { subscribe, set, update } = writable<AuthState>({\n    isAuthenticated: false,\n    user: null,\n    loading: true,\n    error: null,\n  });\n\n  return {\n    subscribe,\n\n    async initialize() {\n      try {\n        const response = await fetch(\"/api/auth/me\");\n        if (response.ok) {\n          const user = await response.json();\n          set({\n            isAuthenticated: true,\n            user,\n            loading: false,\n            error: null,\n          });\n        } else {\n          set({\n            isAuthenticated: false,\n            user: null,\n            loading: false,\n            error: null,\n          });\n        }\n      } catch (error) {\n        set({\n          isAuthenticated: false,\n          user: null,\n          loading: false,\n          error: \"Failed to initialize auth\",\n        });\n      }\n    },\n\n    async login(identifier: string, password: string) {\n      try {\n        const response = await csrfFetch(\"/api/auth/login\", {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ usernameOrEmail: identifier, password }),\n        });\n\n        if (!response.ok) {\n          throw new Error(\"Login failed\");\n        }\n\n        const user = await response.json();\n        set({\n          isAuthenticated: true,\n          user,\n          loading: false,\n          error: null,\n        });\n\n        return true;\n      } catch (error) {\n        update((state: AuthState) => ({\n          ...state,\n          error: \"Login failed\",\n          loading: false,\n        }));\n        return false;\n      }\n    },\n\n    async register(\n      username: string,\n      email: string | null,\n      password: string,\n      captchaId: string,\n      captchaAnswer: string,\n    ) {\n      try {\n        const response = await csrfFetch(\"/api/auth/register\", {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ username, email, password, captchaId, captchaAnswer }),\n        });\n\n        if (!response.ok) {\n          throw new Error(\"Registration failed\");\n        }\n\n        const user = await response.json();\n        set({\n          isAuthenticated: true,\n          user,\n          loading: false,\n          error: null,\n        });\n\n        return true;\n      } catch (error) {\n        update((state: AuthState) => ({\n          ...state,\n          error: \"Registration failed\",\n          loading: false,\n        }));\n        return false;\n      }\n    },\n\n    async logout() {\n      try {\n        await csrfFetch(\"/api/auth/logout\", { method: \"POST\" });\n      } finally {\n        set({\n          isAuthenticated: false,\n          user: null,\n          loading: false,\n          error: null,\n        });\n      }\n    },\n\n    clearError() {\n      update((state: AuthState) => ({ ...state, error: null }));\n    },\n  };\n}\n\nexport const authStore = createAuthStore();\n"
  },
  {
    "path": "src/lib/stores/comments.ts",
    "content": "import { writable } from \"svelte/store\";\nimport type { Comment } from \"$lib/types/comments\";\nimport { csrfFetch } from \"$lib/utils/csrf\";\n\ninterface CommentStore {\n  comments: Comment[];\n  total: number;\n  loading: boolean;\n  error: string | null;\n}\n\nfunction createCommentsStore() {\n  const { subscribe, set, update } = writable<CommentStore>({\n    comments: [],\n    total: 0,\n    loading: false,\n    error: null,\n  });\n\n  return {\n    subscribe,\n    async getComments(\n      mediaType: string,\n      mediaId: number,\n      page = 1,\n      limit = 10,\n    ) {\n      update((state) => ({ ...state, loading: true, error: null }));\n      try {\n        const response = await fetch(\n          `/api/comments?mediaType=${mediaType}&mediaId=${mediaId}&page=${page}&limit=${limit}`,\n        );\n        if (!response.ok) throw new Error(\"Failed to fetch comments\");\n        const data = await response.json();\n        update((state) => ({\n          ...state,\n          comments: data.comments,\n          total: data.total,\n          loading: false,\n        }));\n        return data;\n      } catch (error) {\n        update((state) => ({\n          ...state,\n          error:\n            error instanceof Error ? error.message : \"Failed to fetch comments\",\n          loading: false,\n        }));\n        throw error;\n      }\n    },\n\n    async addComment({\n      mediaType,\n      mediaId,\n      content,\n      rating,\n      parentId,\n    }: {\n      mediaType: string;\n      mediaId: number;\n      content: string;\n      rating?: number;\n      parentId?: number;\n    }) {\n      try {\n        const response = await csrfFetch(\"/api/comments\", {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({\n            mediaType,\n            mediaId,\n            content,\n            rating,\n            parentId,\n          }),\n        });\n        if (!response.ok) throw new Error(\"Failed to add comment\");\n        const newComment = await response.json();\n        update((state) => ({\n          ...state,\n          comments: [newComment, ...state.comments],\n          total: state.total + 1,\n        }));\n        return newComment;\n      } catch (error) {\n        throw error;\n      }\n    },\n\n    async toggleLike(commentId: number) {\n      try {\n        const response = await csrfFetch(`/api/comments/like`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ commentId }),\n        });\n        if (!response.ok) throw new Error(\"Failed to toggle like\");\n        const { liked } = await response.json();\n        update((state) => ({\n          ...state,\n          comments: state.comments.map((comment) => {\n            if (comment.id === commentId) {\n              return {\n                ...comment,\n                isLiked: liked,\n                _count: {\n                  ...comment._count,\n                  likes: comment._count.likes + (liked ? 1 : -1),\n                },\n              };\n            }\n            return comment;\n          }),\n        }));\n      } catch (error) {\n        throw error;\n      }\n    },\n\n    async updateComment(commentId: number, content: string) {\n      try {\n        const response = await csrfFetch(`/api/comments/${commentId}`, {\n          method: \"PUT\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ content }),\n        });\n        if (!response.ok) throw new Error(\"Failed to update comment\");\n        const updatedComment = await response.json();\n        update((state) => ({\n          ...state,\n          comments: state.comments.map((comment) =>\n            comment.id === commentId ? updatedComment : comment,\n          ),\n        }));\n        return updatedComment;\n      } catch (error) {\n        throw error;\n      }\n    },\n\n    async deleteComment(commentId: number) {\n      try {\n        const response = await csrfFetch(`/api/comments/${commentId}`, {\n          method: \"DELETE\",\n        });\n        if (!response.ok) throw new Error(\"Failed to delete comment\");\n        update((state) => ({\n          ...state,\n          comments: state.comments.filter(\n            (comment) => comment.id !== commentId,\n          ),\n          total: state.total - 1,\n        }));\n      } catch (error) {\n        throw error;\n      }\n    },\n  };\n}\n\nexport const commentsStore = createCommentsStore();\n"
  },
  {
    "path": "src/lib/stores/filters.ts",
    "content": "import { writable, derived, get } from \"svelte/store\";\nimport type {\n  FilterState,\n  FilterOptions,\n  Genre,\n  SortBy,\n  SortOrder,\n  WatchStatus,\n} from \"$lib/types/filters\";\nimport { defaultFilterState, createQueryString } from \"$lib/types/filters\";\nimport { goto } from \"$app/navigation\";\nimport { page } from \"$app/stores\";\nimport type { Page } from \"@sveltejs/kit\";\n\nconst sortByValues: SortBy[] = [\"popularity\", \"rating\", \"release_date\", \"title\"];\nconst sortOrderValues: SortOrder[] = [\"asc\", \"desc\"];\nconst watchStatusValues: WatchStatus[] = [\"all\", \"watching\", \"completed\", \"planned\"];\n\nfunction isSortBy(value: string): value is SortBy {\n  return sortByValues.includes(value as SortBy);\n}\n\nfunction isSortOrder(value: string): value is SortOrder {\n  return sortOrderValues.includes(value as SortOrder);\n}\n\nfunction isWatchStatus(value: string): value is WatchStatus {\n  return watchStatusValues.includes(value as WatchStatus);\n}\n\nfunction createFilterStore() {\n  const { subscribe, set, update } = writable<FilterState>(defaultFilterState);\n\n  return {\n    subscribe,\n\n    initialize(genres: Genre[]) {\n      update((state) => ({\n        ...state,\n        availableGenres: genres,\n      }));\n    },\n\n    setFromQueryString(queryString: string) {\n      const params = new URLSearchParams(queryString);\n\n      update((state) => {\n        const newState = { ...state };\n\n\n        const query = params.get(\"query\");\n        if (query) newState.query = query;\n\n\n        const genres = params.get(\"genres\");\n        if (genres) {\n          newState.genres = genres.split(\",\").map(Number);\n        }\n\n\n        const year = params.get(\"year\");\n        if (year) newState.year = parseInt(year);\n\n\n        const rating = params.get(\"rating\");\n        if (rating) newState.rating = parseInt(rating);\n\n\n        const sortBy = params.get(\"sortBy\");\n        if (sortBy && isSortBy(sortBy)) newState.sortBy = sortBy;\n\n        const sortOrder = params.get(\"sortOrder\");\n        if (sortOrder && isSortOrder(sortOrder)) newState.sortOrder = sortOrder;\n\n\n        const watchStatus = params.get(\"watchStatus\");\n        if (watchStatus && isWatchStatus(watchStatus)) {\n          newState.watchStatus = watchStatus;\n        }\n\n\n        const page = params.get(\"page\");\n        if (page) newState.page = parseInt(page);\n\n        const limit = params.get(\"limit\");\n        if (limit) newState.limit = parseInt(limit);\n\n        return newState;\n      });\n    },\n\n    updateFilters(filters: Partial<FilterOptions>, navigate = true) {\n      update((state) => {\n        const newState = {\n          ...state,\n          ...filters,\n\n          page: \"page\" in filters ? filters.page || 1 : 1,\n        };\n\n        if (navigate) {\n          const currentPage = get(page);\n          const baseUrl = currentPage.url.pathname;\n          const queryString = createQueryString(newState);\n          goto(`${baseUrl}?${queryString}`, { replaceState: true });\n        }\n\n        return newState;\n      });\n    },\n\n    reset() {\n      update((state) => ({\n        ...defaultFilterState,\n        availableGenres: state.availableGenres,\n      }));\n\n      const currentPage = get(page);\n      const baseUrl = currentPage.url.pathname;\n      goto(baseUrl, { replaceState: true });\n    },\n\n    setResults(totalResults: number, totalPages: number) {\n      update((state) => ({\n        ...state,\n        totalResults,\n        totalPages,\n      }));\n    },\n  };\n}\n\nexport const filters = createFilterStore();\n\n\nexport const activeFilters = derived(filters, ($filters) => {\n  const active: string[] = [];\n\n  if ($filters.query) {\n    active.push(`Search: \"${$filters.query}\"`);\n  }\n\n  if ($filters.genres && $filters.genres.length > 0) {\n    const genreNames = $filters.genres\n      .map((id) => $filters.availableGenres.find((g) => g.id === id)?.name)\n      .filter(Boolean);\n    if (genreNames.length > 0) {\n      active.push(`Genres: ${genreNames.join(\", \")}`);\n    }\n  }\n\n  if ($filters.year) {\n    active.push(`Year: ${$filters.year}`);\n  }\n\n  if ($filters.rating) {\n    active.push(`Rating: ${$filters.rating}+`);\n  }\n\n  if ($filters.watchStatus && $filters.watchStatus !== \"all\") {\n    active.push(`Status: ${$filters.watchStatus}`);\n  }\n\n  if ($filters.sortBy && $filters.sortBy !== \"popularity\") {\n    active.push(`Sort: ${$filters.sortBy} (${$filters.sortOrder})`);\n  }\n\n  return active;\n});\n\nexport const hasActiveFilters = derived(\n  activeFilters,\n  ($activeFilters) => $activeFilters.length > 0,\n);\n\nexport const currentPage = derived(filters, ($filters) => $filters.page || 1);\n\nexport const totalPages = derived(filters, ($filters) => $filters.totalPages);\n\nexport const selectedGenres = derived(filters, ($filters) => {\n  if (!$filters.genres) return [];\n  return $filters.availableGenres.filter((genre) =>\n    $filters.genres?.includes(genre.id),\n  );\n});\n"
  },
  {
    "path": "src/lib/stores/provider-urls.ts",
    "content": "import { writable } from \"svelte/store\";\n\ninterface ProviderUrls {\n  vidsrc: string;\n  vidlink: string;\n  movies111: string;\n  embed2: string;\n}\n\nexport const providerUrls = writable<ProviderUrls | null>(null);\n\nexport async function loadProviderUrls() {\n  if (typeof window === \"undefined\") return;\n\n  try {\n    const response = await fetch(\"/api/providers\");\n    const urls = await response.json();\n    providerUrls.set(urls);\n  } catch (error) {\n    console.error(\"Failed to load provider URLs:\", error);\n  }\n}\n"
  },
  {
    "path": "src/lib/stores/toast.ts",
    "content": "import { writable } from \"svelte/store\";\n\nexport interface Toast {\n  id: string;\n  type: \"success\" | \"error\" | \"info\" | \"warning\";\n  message: string;\n  duration?: number;\n}\n\nfunction createToastStore() {\n  const { subscribe, update } = writable<Toast[]>([]);\n\n  function addToast(toast: Omit<Toast, \"id\">) {\n    const id = Math.random().toString(36).substring(2);\n    const duration = toast.duration || 5000;\n\n    update((toasts) => [...toasts, { ...toast, id }]);\n\n\n    setTimeout(() => {\n      removeToast(id);\n    }, duration);\n  }\n\n  function removeToast(id: string) {\n    update((toasts) => toasts.filter((t) => t.id !== id));\n  }\n\n  function success(message: string, duration?: number) {\n    addToast({ type: \"success\", message, duration });\n  }\n\n  function error(message: string, duration?: number) {\n    addToast({ type: \"error\", message, duration });\n  }\n\n  function info(message: string, duration?: number) {\n    addToast({ type: \"info\", message, duration });\n  }\n\n  function warning(message: string, duration?: number) {\n    addToast({ type: \"warning\", message, duration });\n  }\n\n  return {\n    subscribe,\n    success,\n    error,\n    info,\n    warning,\n    remove: removeToast,\n  };\n}\n\nexport const toastStore = createToastStore();\n"
  },
  {
    "path": "src/lib/stores/watchlist.ts",
    "content": "import { writable } from \"svelte/store\";\nimport { authStore } from \"./auth\";\nimport { get } from \"svelte/store\";\nimport { csrfFetch } from \"$lib/utils/csrf\";\n\ninterface WatchlistItem {\n  id: number;\n  mediaId: number;\n  mediaType: string;\n  title: string;\n  posterPath: string | null;\n  voteAverage: number;\n  addedAt: string;\n}\n\ninterface WatchlistStore {\n  items: WatchlistItem[];\n  total: number;\n  loading: boolean;\n  error: string | null;\n}\n\nfunction createWatchlistStore() {\n  const { subscribe, set, update } = writable<WatchlistStore>({\n    items: [],\n    total: 0,\n    loading: false,\n    error: null,\n  });\n\n  return {\n    subscribe,\n    async getWatchlist() {\n      const auth = get(authStore);\n      if (!auth.user) {\n        update(state => ({ ...state, items: [], total: 0, loading: false, error: null }));\n        return { items: [], total: 0 };\n      }\n\n      update((state) => ({ ...state, loading: true, error: null }));\n      try {\n        const response = await fetch(\"/api/watchlist\");\n        if (!response.ok) throw new Error(\"Failed to fetch watchlist\");\n        const data = await response.json();\n        update((state) => ({\n          ...state,\n          items: data.items,\n          total: data.total,\n          loading: false,\n        }));\n        return data;\n      } catch (error) {\n        update((state) => ({\n          ...state,\n          error:\n            error instanceof Error\n              ? error.message\n              : \"Failed to fetch watchlist\",\n          loading: false,\n        }));\n        throw error;\n      }\n    },\n\n    async addToWatchlist(\n      mediaId: number,\n      mediaType: string,\n      title: string,\n      posterPath: string | null,\n      voteAverage: number,\n    ) {\n      const auth = get(authStore);\n      if (!auth.user) {\n        throw new Error(\"Must be logged in to add to watchlist\");\n      }\n\n      try {\n        const response = await csrfFetch(\"/api/watchlist\", {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({\n            mediaId,\n            mediaType,\n            title,\n            posterPath,\n            voteAverage,\n          }),\n        });\n        if (!response.ok) throw new Error(\"Failed to add to watchlist\");\n        const newItem = await response.json();\n        update((state) => ({\n          ...state,\n          items: [newItem, ...state.items],\n          total: state.total + 1,\n        }));\n        return newItem;\n      } catch (error) {\n        throw error;\n      }\n    },\n\n    async removeFromWatchlist(mediaId: number, mediaType: string) {\n      const auth = get(authStore);\n      if (!auth.user) {\n        throw new Error(\"Must be logged in to remove from watchlist\");\n      }\n\n      try {\n        const response = await csrfFetch(\"/api/watchlist\", {\n          method: \"DELETE\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ mediaId, mediaType }),\n        });\n        if (!response.ok) throw new Error(\"Failed to remove from watchlist\");\n        update((state) => ({\n          ...state,\n          items: state.items.filter(\n            (item) =>\n              !(item.mediaId === mediaId && item.mediaType === mediaType),\n          ),\n          total: state.total - 1,\n        }));\n      } catch (error) {\n        throw error;\n      }\n    },\n\n    async isInWatchlist(mediaId: number, mediaType: string) {\n      const auth = get(authStore);\n      if (!auth.user) {\n        return false;\n      }\n\n      try {\n        const response = await fetch(\n          `/api/watchlist/check?mediaId=${mediaId}&mediaType=${mediaType}`,\n        );\n        if (!response.ok) throw new Error(\"Failed to check watchlist status\");\n        const { inWatchlist } = await response.json();\n        return inWatchlist;\n      } catch (error) {\n        throw error;\n      }\n    },\n\n    reset() {\n      set({\n        items: [],\n        total: 0,\n        loading: false,\n        error: null,\n      });\n    }\n  };\n}\n\nexport const watchlistStore = createWatchlistStore();\n"
  },
  {
    "path": "src/lib/types/auth.ts",
    "content": "export interface UserSession {\n  id: number;\n  username: string;\n  email: string | null;\n  isAdmin: boolean;\n}\n\nexport interface TokenPayload {\n  userId: number;\n}\n\nexport interface AuthServiceInterface {\n  hashPassword(password: string): Promise<string>;\n  comparePasswords(password: string, hash: string): Promise<boolean>;\n  generateToken(user: UserSession): Promise<string>;\n  verifyToken(token: string): Promise<TokenPayload>;\n  createUser(\n    username: string,\n    email: string | null,\n    password: string,\n  ): Promise<UserSession>;\n  validateUser(\n    usernameOrEmail: string,\n    password: string,\n  ): Promise<UserSession | null>;\n  findUserByIdentifier(identifier: string): Promise<UserSession | null>;\n  updatePassword(userId: number, newPassword: string): Promise<void>;\n  createResetToken(identifier: string): Promise<string | null>;\n  validateResetToken(token: string): Promise<number | null>;\n  clearResetToken(userId: number): Promise<void>;\n}\n\n\nexport async function verifyToken(token: string): Promise<TokenPayload> {\n  const { authService } = await import(\"$lib/server/services/auth\");\n  return authService.verifyToken(token);\n}\n\n\nexport const userSelect = {\n  id: true,\n  username: true,\n  email: true,\n  isAdmin: true,\n} as const;\n"
  },
  {
    "path": "src/lib/types/comments.ts",
    "content": "export interface Comment {\n  id: number;\n  content: string;\n  userId: number;\n  mediaId: number;\n  mediaType: string;\n  rating: number | null;\n  parentId: number | null;\n  createdAt: string;\n  updatedAt: string;\n  user: {\n    username: string;\n  };\n  _count: {\n    likes: number;\n    replies: number;\n  };\n  isLiked?: boolean;\n  flagged?: boolean;\n  flagReason?: string | null;\n  flaggedAt?: string | null;\n}\n\nexport interface CommentResponse {\n  comments: Comment[];\n  total: number;\n}\n"
  },
  {
    "path": "src/lib/types/filters.ts",
    "content": "export type SortOrder = \"asc\" | \"desc\";\nexport type SortBy = \"popularity\" | \"rating\" | \"release_date\" | \"title\";\nexport type MediaType = \"movie\" | \"tv\";\nexport type WatchStatus = \"all\" | \"watching\" | \"completed\" | \"planned\";\n\nexport interface Genre {\n  id: number;\n  name: string;\n}\n\nexport interface FilterOptions {\n  query?: string;\n  genres?: number[];\n  year?: number;\n  rating?: number;\n  sortBy?: SortBy;\n  sortOrder?: SortOrder;\n  watchStatus?: WatchStatus;\n  page?: number;\n  limit?: number;\n}\n\nexport interface FilterState extends FilterOptions {\n  availableGenres: Genre[];\n  totalResults: number;\n  totalPages: number;\n}\n\nexport const defaultFilterState: FilterState = {\n  query: \"\",\n  genres: [],\n  sortBy: \"popularity\",\n  sortOrder: \"desc\",\n  watchStatus: \"all\",\n  page: 1,\n  limit: 20,\n  availableGenres: [],\n  totalResults: 0,\n  totalPages: 0,\n};\n\nexport const sortOptions: { value: SortBy; label: string }[] = [\n  { value: \"popularity\", label: \"Popularity\" },\n  { value: \"rating\", label: \"Rating\" },\n  { value: \"release_date\", label: \"Release Date\" },\n  { value: \"title\", label: \"Title\" },\n];\n\nexport const watchStatusOptions: { value: WatchStatus; label: string }[] = [\n  { value: \"all\", label: \"All\" },\n  { value: \"watching\", label: \"Watching\" },\n  { value: \"completed\", label: \"Completed\" },\n  { value: \"planned\", label: \"Plan to Watch\" },\n];\n\nexport const yearOptions: { value: number; label: string }[] = (() => {\n  const currentYear = new Date().getFullYear();\n  const years = [];\n  for (let year = currentYear; year >= 1900; year--) {\n    years.push({ value: year, label: year.toString() });\n  }\n  return years;\n})();\n\nexport const ratingOptions: { value: number; label: string }[] = [\n  { value: 9, label: \"9+ Rating\" },\n  { value: 8, label: \"8+ Rating\" },\n  { value: 7, label: \"7+ Rating\" },\n  { value: 6, label: \"6+ Rating\" },\n  { value: 5, label: \"5+ Rating\" },\n  { value: 0, label: \"All Ratings\" },\n];\n\nexport function createQueryString(filters: FilterOptions): string {\n  const params = new URLSearchParams();\n\n  if (filters.query) {\n    params.set(\"query\", filters.query);\n  }\n\n  if (filters.genres && filters.genres.length > 0) {\n    params.set(\"genres\", filters.genres.join(\",\"));\n  }\n\n  if (filters.year) {\n    params.set(\"year\", filters.year.toString());\n  }\n\n  if (filters.rating) {\n    params.set(\"rating\", filters.rating.toString());\n  }\n\n  if (filters.sortBy) {\n    params.set(\"sortBy\", filters.sortBy);\n  }\n\n  if (filters.sortOrder) {\n    params.set(\"sortOrder\", filters.sortOrder);\n  }\n\n  if (filters.watchStatus && filters.watchStatus !== \"all\") {\n    params.set(\"watchStatus\", filters.watchStatus);\n  }\n\n  if (filters.page && filters.page > 1) {\n    params.set(\"page\", filters.page.toString());\n  }\n\n  if (filters.limit && filters.limit !== 20) {\n    params.set(\"limit\", filters.limit.toString());\n  }\n\n  return params.toString();\n}\n\nexport function parseQueryString(queryString: string): FilterOptions {\n  const params = new URLSearchParams(queryString);\n  const filters: FilterOptions = {};\n\n  const query = params.get(\"query\");\n  if (query) {\n    filters.query = query;\n  }\n\n  const genres = params.get(\"genres\");\n  if (genres) {\n    filters.genres = genres.split(\",\").map(Number);\n  }\n\n  const year = params.get(\"year\");\n  if (year) {\n    filters.year = parseInt(year);\n  }\n\n  const rating = params.get(\"rating\");\n  if (rating) {\n    filters.rating = parseInt(rating);\n  }\n\n  const sortBy = params.get(\"sortBy\") as SortBy;\n  if (sortBy) {\n    filters.sortBy = sortBy;\n  }\n\n  const sortOrder = params.get(\"sortOrder\") as SortOrder;\n  if (sortOrder) {\n    filters.sortOrder = sortOrder;\n  }\n\n  const watchStatus = params.get(\"watchStatus\") as WatchStatus;\n  if (watchStatus) {\n    filters.watchStatus = watchStatus;\n  }\n\n  const page = params.get(\"page\");\n  if (page) {\n    filters.page = parseInt(page);\n  }\n\n  const limit = params.get(\"limit\");\n  if (limit) {\n    filters.limit = parseInt(limit);\n  }\n\n  return filters;\n}\n"
  },
  {
    "path": "src/lib/types/provider.ts",
    "content": "export interface MediaProvider {\n  id: string;\n  name: string;\n  supportsMovies: boolean;\n  supportsTVShows: boolean;\n  requiresLanguage?: boolean;\n  languages?: string[];\n  getMovieUrl: (\n    mediaId: string | number,\n    options?: ProviderOptions,\n  ) => Promise<string>;\n  getTVShowUrl?: (\n    mediaId: string | number,\n    seasonId: number,\n    episodeId: number,\n    options?: ProviderOptions,\n  ) => Promise<string>;\n}\n\nexport interface ProviderOptions {\n  language?: string;\n  primaryColor?: string;\n  secondaryColor?: string;\n  iconColor?: string;\n  autoPlay?: boolean;\n  autoNext?: boolean;\n}\n\nexport interface StreamingQuality {\n  quality: string;\n  url: string;\n  metadata?: {\n    baseUrl?: string;\n    [key: string]: unknown;\n  };\n}\n\nexport interface ProviderResponse {\n  url: string;\n  type: \"iframe\" | \"hls\" | \"dash\";\n  qualities?: StreamingQuality[];\n}\n"
  },
  {
    "path": "src/lib/types/tmdb.ts",
    "content": "export interface TMDBReleaseDatesResponse {\n  id: number;\n  results: TMDBReleaseDateResult[];\n}\n\nexport interface TMDBReleaseDateResult {\n  iso_3166_1: string;\n  release_dates: TMDBReleaseDate[];\n}\n\nexport interface TMDBReleaseDate {\n  certification: string;\n  iso_639_1?: string;\n  release_date: string;\n  type: number;\n  note?: string;\n}\n\nexport interface TMDBMediaResponse {\n  id: number;\n  title?: string;\n  name?: string;\n  overview: string;\n  poster_path: string | null;\n  backdrop_path: string | null;\n  release_date?: string;\n  first_air_date?: string;\n  vote_average: number;\n  vote_count: number;\n  genre_ids: number[];\n  media_type?: \"movie\" | \"tv\";\n  popularity: number;\n}\n\nexport interface TMDBResponse<T> {\n  page: number;\n  results: T[];\n  total_pages: number;\n  total_results: number;\n}\n\nexport interface TMDBVideoResponse {\n  id: number;\n  results: TMDBVideo[];\n}\n\nexport interface TMDBVideo {\n  id: string;\n  key: string;\n  name: string;\n  site: string;\n  size: number;\n  type: string;\n}\n\nexport interface TMDBMovie extends TMDBMediaResponse {\n  title: string;\n  release_date: string;\n  media_type: \"movie\";\n}\n\nexport interface TMDBTVShow extends TMDBMediaResponse {\n  name: string;\n  first_air_date: string;\n  media_type: \"tv\";\n}\n\nexport interface TMDBGenre {\n  id: number;\n  name: string;\n}\n\nexport interface TMDBWatchProvider {\n  logo_path: string;\n  provider_id: number;\n  provider_name: string;\n  display_priority: number;\n}\n\nexport interface TMDBWatchProviderRegion {\n  link?: string;\n  flatrate?: TMDBWatchProvider[];\n  rent?: TMDBWatchProvider[];\n  buy?: TMDBWatchProvider[];\n}\n\nexport interface TMDBWatchProvidersResponse {\n  id: number;\n  results: Record<string, TMDBWatchProviderRegion>;\n}\n"
  },
  {
    "path": "src/lib/utils/csrf.ts",
    "content": "import { CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from \"$lib/constants/security\";\n\nfunction getCookie(name: string): string | null {\n  if (typeof document === \"undefined\") return null;\n\n  const escapedName = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n  const match = document.cookie.match(new RegExp(`(?:^|; )${escapedName}=([^;]*)`));\n  return match ? decodeURIComponent(match[1]) : null;\n}\n\nexport function getCsrfToken(): string | null {\n  return getCookie(CSRF_COOKIE_NAME);\n}\n\nexport function withCsrfHeaders(init: RequestInit = {}): RequestInit {\n  const token = getCsrfToken();\n  if (!token) return init;\n\n  const headers = new Headers(init.headers ?? {});\n  headers.set(CSRF_HEADER_NAME, token);\n\n  return {\n    ...init,\n    headers,\n  };\n}\n\nexport async function csrfFetch(\n  input: RequestInfo | URL,\n  init: RequestInit = {},\n): Promise<Response> {\n  const method = (init.method ?? \"GET\").toUpperCase();\n  if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n    return fetch(input, init);\n  }\n\n  return fetch(input, withCsrfHeaders(init));\n}\n"
  },
  {
    "path": "src/routes/+layout.server.ts",
    "content": "import type { ServerLoad } from \"@sveltejs/kit\";\n\nexport const load: ServerLoad = async ({ locals }) => {\n  return {\n    user: locals.user,\n  };\n};\n"
  },
  {
    "path": "src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n  import '../app.css';\n  import Navbar from '$lib/components/Navbar.svelte';\n  import { onMount } from 'svelte';\n  import { browser } from '$app/environment';\n  import { watchlistStore } from '$lib/stores/watchlist';\n  import { authStore } from '$lib/stores/auth';\n  import { loadProviderUrls } from '$lib/stores/provider-urls';\n\n  onMount(async () => {\n    if (browser) {\n      try {\n\n        await authStore.initialize();\n\n\n        await loadProviderUrls();\n\n\n        if ($authStore.isAuthenticated) {\n          await watchlistStore.getWatchlist();\n        }\n      } catch (error) {\n        console.error('Failed to load initial data:', error);\n      }\n    }\n  });\n</script>\n\n<div class=\"min-h-screen bg-gray-900 text-white\">\n  <Navbar />\n\n  <main>\n    <slot />\n  </main>\n\n  <footer class=\"bg-gray-800 py-8 mt-16\">\n    <div class=\"container mx-auto px-4\">\n      <div class=\"flex flex-col md:flex-row justify-between items-center gap-4\">\n        <div class=\"text-gray-400\">\n          <p class=\"text-[12px] text-gray-400\">This website doesn't host any file. Legal issues must be discussed with files hosts & providers. We are not affiliated with 3rd party providers & we have no control over the content they distribute.</p>\n        </div>\n        <div class=\"flex items-center gap-6\">\n          <a href=\"/dmca\" class=\"text-gray-400 hover:text-white transition-colors\">DMCA</a>\n          <a\n            href=\"https://github.com/gmonarque/streamium\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            class=\"text-gray-400 hover:text-white transition-colors flex items-center gap-2\"\n          >\n            <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n              <path fill-rule=\"evenodd\" d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\" clip-rule=\"evenodd\" />\n            </svg>\n            <span>GitHub</span>\n          </a>\n        </div>\n      </div>\n    </div>\n  </footer>\n</div>\n\n<style>\n  :global(body) {\n    @apply bg-gray-900;\n  }\n</style>\n"
  },
  {
    "path": "src/routes/+layout.ts",
    "content": "import { browser } from \"$app/environment\";\nimport { watchlistStore } from \"$lib/stores/watchlist\";\nimport { authStore } from \"$lib/stores/auth\";\nimport { get } from \"svelte/store\";\n\nexport const load = async () => {\n  if (browser) {\n    const auth = get(authStore);\n    if (auth.user) {\n      try {\n        await watchlistStore.getWatchlist();\n      } catch (error) {\n        console.error(\"Failed to load watchlist:\", error);\n      }\n    } else {\n\n      watchlistStore.reset();\n    }\n  }\n};\n"
  },
  {
    "path": "src/routes/+page.server.ts",
    "content": "import type { ServerLoad } from \"@sveltejs/kit\";\nimport { TMDBService, TMDBApiError } from \"$lib/services/tmdb\";\n\nexport const load = (async () => {\n  const tmdb = new TMDBService();\n\n  if (!tmdb.isConfigured()) {\n    return {\n      trendingMovies: [],\n      trendingTVShows: [],\n      error: 'TMDB API key is not configured. Please add TMDB_API_KEY to your .env file.',\n    };\n  }\n\n  try {\n    const [trendingMovies, trendingTVShows] = await Promise.all([\n      tmdb.getTrendingMovies(),\n      tmdb.getTrendingTVShows(),\n    ]);\n\n    return {\n      trendingMovies: trendingMovies.results.slice(0, 12),\n      trendingTVShows: trendingTVShows.results.slice(0, 12),\n      error: null,\n    };\n  } catch (err) {\n    const error = err instanceof TMDBApiError\n      ? err.message\n      : 'Failed to load content. Please try again later.';\n\n    console.error('Homepage load error:', err);\n\n    return {\n      trendingMovies: [],\n      trendingTVShows: [],\n      error,\n    };\n  }\n}) satisfies ServerLoad;\n"
  },
  {
    "path": "src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import MediaCard from '$lib/components/MediaCard.svelte';\n  import Hero from '$lib/components/Hero.svelte';\n  import { authStore } from '$lib/stores/auth';\n  import type { TMDBMediaResponse } from '$lib/types/tmdb';\n\n  let trendingMovies: TMDBMediaResponse[] = [];\n  let trendingShows: TMDBMediaResponse[] = [];\n  let featuredMedia: TMDBMediaResponse | null = null;\n  let featuredType: 'movie' | 'tv' = 'movie';\n  let loading = true;\n  let error: string | null = null;\n\n  async function fetchTrending() {\n    try {\n      const [moviesRes, showsRes] = await Promise.all([\n        fetch('/api/movies?sort=trending'),\n        fetch('/api/tv?sort=trending')\n      ]);\n\n      const movies = await moviesRes.json();\n      const shows = await showsRes.json();\n\n      // Check for API errors\n      if (movies.error || shows.error) {\n        error = movies.error || shows.error;\n        trendingMovies = [];\n        trendingShows = [];\n        return;\n      }\n\n      trendingMovies = movies.results.slice(0, 6);\n      trendingShows = shows.results.slice(0, 6);\n\n      const allMedia = [...movies.results, ...shows.results];\n      if (allMedia.length > 0) {\n        const randomIndex = Math.floor(Math.random() * allMedia.length);\n        featuredMedia = allMedia[randomIndex];\n        featuredType = featuredMedia && 'title' in featuredMedia ? 'movie' : 'tv';\n      }\n    } catch (err) {\n      console.error('Error fetching trending content:', err);\n      error = 'Failed to load trending content. Please try again later.';\n    } finally {\n      loading = false;\n    }\n  }\n\n  onMount(() => {\n    fetchTrending();\n  });\n</script>\n\n{#if featuredMedia}\n  <Hero media={featuredMedia} type={featuredType} />\n{/if}\n\n<div class=\"container mx-auto px-4 py-8 space-y-12\">\n  <section>\n    <div class=\"flex justify-between items-center mb-6\">\n      <h2 class=\"text-2xl font-bold\">Trending Movies</h2>\n      <a\n        href=\"/movies?sort=trending\"\n        class=\"text-primary-400 hover:text-primary-300 font-medium flex items-center gap-1\"\n      >\n        Show All\n        <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n        </svg>\n      </a>\n    </div>\n\n    {#if loading}\n      <div class=\"flex justify-center py-8\">\n        <div class=\"animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500\"></div>\n      </div>\n    {:else if error}\n      <div class=\"bg-yellow-900/30 border border-yellow-700 rounded-lg p-6 text-center\">\n        <svg class=\"w-12 h-12 mx-auto mb-4 text-yellow-500\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n        </svg>\n        <p class=\"text-yellow-200 mb-2\">{error}</p>\n        <p class=\"text-gray-400 text-sm\">Get a free API key at <a href=\"https://www.themoviedb.org/settings/api\" target=\"_blank\" rel=\"noopener\" class=\"text-primary-400 hover:underline\">themoviedb.org</a></p>\n      </div>\n    {:else if trendingMovies.length === 0}\n      <div class=\"text-gray-400 text-center py-8\">\n        No trending movies available\n      </div>\n    {:else}\n      <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">\n        {#each trendingMovies as movie (movie.id)}\n          <MediaCard\n            id={movie.id}\n            type=\"movie\"\n            title={movie.title || ''}\n            posterPath={movie.poster_path}\n            voteAverage={movie.vote_average}\n          />\n        {/each}\n      </div>\n    {/if}\n  </section>\n\n  <section>\n    <div class=\"flex justify-between items-center mb-6\">\n      <h2 class=\"text-2xl font-bold\">Trending TV Shows</h2>\n      <a\n        href=\"/tv?sort=trending\"\n        class=\"text-primary-400 hover:text-primary-300 font-medium flex items-center gap-1\"\n      >\n        Show All\n        <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n        </svg>\n      </a>\n    </div>\n\n    {#if loading}\n      <div class=\"flex justify-center py-8\">\n        <div class=\"animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500\"></div>\n      </div>\n    {:else if error}\n      <!-- Error already shown above -->\n    {:else if trendingShows.length === 0}\n      <div class=\"text-gray-400 text-center py-8\">\n        No trending TV shows available\n      </div>\n    {:else}\n      <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">\n        {#each trendingShows as show (show.id)}\n          <MediaCard\n            id={show.id}\n            type=\"tv\"\n            title={show.name || ''}\n            posterPath={show.poster_path}\n            voteAverage={show.vote_average}\n          />\n        {/each}\n      </div>\n    {/if}\n  </section>\n</div>\n"
  },
  {
    "path": "src/routes/admin/moderation/+page.server.ts",
    "content": "import { redirect } from '@sveltejs/kit';\nimport type { PageServerLoad } from './$types';\n\nexport const load: PageServerLoad = async ({ locals }) => {\n  if (!locals.user?.isAdmin) {\n    throw redirect(303, '/');\n  }\n\n  return {\n    user: locals.user\n  };\n};\n"
  },
  {
    "path": "src/routes/admin/moderation/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import { page } from '$app/stores';\n  import { goto } from '$app/navigation';\n  import type { CommentWithDetails } from '$lib/services/comments';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  let comments: CommentWithDetails[] = [];\n  let loading = true;\n  let error = '';\n\n  onMount(async () => {\n    if (!$page.data.user?.isAdmin) {\n      goto('/');\n      return;\n    }\n    await loadFlaggedComments();\n  });\n\n  async function loadFlaggedComments() {\n    try {\n      const response = await fetch('/api/comments/flagged');\n      if (!response.ok) throw new Error('Failed to load flagged comments');\n      const data = await response.json();\n      comments = data.comments;\n    } catch (err) {\n      error = err instanceof Error ? err.message : 'Failed to load comments';\n      console.error('Error loading flagged comments:', err);\n    } finally {\n      loading = false;\n    }\n  }\n\n  async function deleteComment(commentId: number) {\n    if (!confirm('Are you sure you want to delete this comment?')) return;\n\n    try {\n      const response = await csrfFetch(`/api/comments/${commentId}`, {\n        method: 'DELETE'\n      });\n\n      if (!response.ok) throw new Error('Failed to delete comment');\n\n      await loadFlaggedComments();\n    } catch (err) {\n      error = err instanceof Error ? err.message : 'Failed to delete comment';\n      console.error('Error deleting comment:', err);\n    }\n  }\n\n  async function unflagComment(commentId: number) {\n    try {\n      const response = await csrfFetch(`/api/comments/${commentId}/unflag`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        }\n      });\n\n      if (!response.ok) throw new Error('Failed to unflag comment');\n\n      await loadFlaggedComments();\n    } catch (err) {\n      error = err instanceof Error ? err.message : 'Failed to unflag comment';\n      console.error('Error unflagging comment:', err);\n    }\n  }\n\n  function formatDate(date: string | Date | null | undefined) {\n    if (!date) return 'Unknown date';\n    return new Date(date).toLocaleString();\n  }\n</script>\n\n<div class=\"container mx-auto px-4 py-8\">\n  <div class=\"flex justify-between items-center mb-8\">\n    <h1 class=\"text-3xl font-bold text-white\">Comment Moderation</h1>\n  </div>\n\n  {#if loading}\n    <div class=\"text-center py-8\">\n      <div class=\"animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500 mx-auto\"></div>\n    </div>\n  {:else if error}\n    <div class=\"bg-red-500 bg-opacity-10 border border-red-500 text-red-500 px-4 py-3 rounded mb-4\">\n      {error}\n    </div>\n  {:else if comments.length === 0}\n    <div class=\"text-center py-8 text-gray-400\">\n      No flagged comments found\n    </div>\n  {:else}\n    <div class=\"space-y-6\">\n      {#each comments as comment (comment.id)}\n        <div class=\"bg-gray-800 rounded-lg p-6\">\n          <div class=\"flex justify-between items-start mb-4\">\n            <div>\n              <p class=\"text-sm text-gray-400\">\n                Posted by {comment.user.username} on {formatDate(comment.createdAt)}\n              </p>\n              <p class=\"text-sm text-red-400 mt-1\">\n                Flagged on {formatDate(comment.flaggedAt)}\n                {#if comment.flagReason}\n                  - Reason: {comment.flagReason}\n                {/if}\n              </p>\n            </div>\n            <div class=\"flex gap-2\">\n              <button\n                class=\"px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm transition-colors\"\n                on:click={() => unflagComment(comment.id)}\n              >\n                Unflag\n              </button>\n              <button\n                class=\"px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded-md text-sm transition-colors\"\n                on:click={() => deleteComment(comment.id)}\n              >\n                Delete\n              </button>\n            </div>\n          </div>\n\n          <p class=\"text-white whitespace-pre-wrap\">{comment.content}</p>\n\n          <div class=\"mt-4 text-sm text-gray-400\">\n            <p>Media: {comment.mediaType} #{comment.mediaId}</p>\n            <p>Likes: {comment._count?.likes || 0}</p>\n            <p>Replies: {comment._count?.replies || 0}</p>\n          </div>\n        </div>\n      {/each}\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "src/routes/api/auth/login/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { authService } from \"$lib/server/services/auth\";\nimport { createSessionCookie, createCsrfCookie, createCsrfToken } from \"$lib/server/auth\";\nimport { RateLimitService } from \"$lib/server/services/rate-limit\";\nimport { handleDatabaseError } from \"$lib/server/services/db-error\";\nimport { dev } from \"$app/environment\";\n\nexport async function POST({ request, getClientAddress }: RequestEvent) {\n  const clientIp = getClientAddress();\n\n  const rateLimit = RateLimitService.checkLoginLimit(clientIp);\n  if (!rateLimit.allowed) {\n    return json(\n      { error: `Too many login attempts. Please try again in ${Math.ceil(rateLimit.timeLeft! / 60)} minutes.` },\n      { status: 429 }\n    );\n  }\n\n  try {\n    const { usernameOrEmail, identifier, password } = await request.json();\n    const loginIdentifier = usernameOrEmail ?? identifier;\n\n    if (!loginIdentifier || !password) {\n      return json({ error: \"Username/Email and password are required\" }, { status: 400 });\n    }\n\n    const user = await authService.validateUser(loginIdentifier, password);\n    if (!user) {\n      return json({ error: \"Invalid credentials\" }, { status: 401 });\n    }\n\n    const token = await authService.generateToken(user);\n    const isProduction = !dev;\n    const csrfToken = createCsrfToken();\n    const headers = new Headers();\n    headers.append(\"Set-Cookie\", createSessionCookie(token, isProduction));\n    headers.append(\"Set-Cookie\", createCsrfCookie(csrfToken, isProduction));\n\n    return json(user, {\n      headers,\n    });\n  } catch (error) {\n    return handleDatabaseError(error, \"login\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/auth/logout/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { clearSessionCookie, clearCsrfCookie } from \"$lib/server/auth\";\n\nexport async function POST(_event: RequestEvent) {\n  try {\n    const headers = new Headers();\n    headers.append(\"Set-Cookie\", clearSessionCookie());\n    headers.append(\"Set-Cookie\", clearCsrfCookie());\n\n    return json(\n      { success: true },\n      {\n        headers,\n      },\n    );\n  } catch (error) {\n    console.error(\"Logout error:\", error);\n    return new Response(\"Internal server error\", { status: 500 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/auth/me/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { prisma } from \"$lib/server/prisma\";\nimport { getSession } from \"$lib/server/auth\";\n\nexport async function GET({ cookies }: RequestEvent) {\n  try {\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return new Response(null, { status: 401 });\n    }\n\n    const user = await prisma.user.findUnique({\n      where: { id: session.userId },\n      select: {\n        id: true,\n        username: true,\n        email: true,\n        createdAt: true,\n        updatedAt: true,\n      },\n    });\n\n    if (!user) {\n      return new Response(null, { status: 401 });\n    }\n\n    return json(user);\n  } catch (error) {\n    console.error(\"Error fetching user:\", error);\n    return new Response(\"Internal server error\", { status: 500 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/auth/register/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { authService } from \"$lib/server/services/auth\";\nimport { createSessionCookie, createCsrfCookie, createCsrfToken } from \"$lib/server/auth\";\nimport { RateLimitService } from \"$lib/server/services/rate-limit\";\nimport { handleDatabaseError } from \"$lib/server/services/db-error\";\nimport { dev } from \"$app/environment\";\nimport { CaptchaService } from \"$lib/server/services/captcha\";\n\nconst PASSWORD_MIN_LENGTH = 8;\nconst PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$/;\n\nexport async function POST({ request, getClientAddress }: RequestEvent) {\n  const clientIp = getClientAddress();\n\n  const rateLimit = RateLimitService.checkRegisterLimit(clientIp);\n  if (!rateLimit.allowed) {\n    return json(\n      { error: `Too many registration attempts. Please try again in ${Math.ceil(rateLimit.timeLeft! / 60)} minutes.` },\n      { status: 429 }\n    );\n  }\n\n  try {\n    const { username, email, password, captchaId, captchaAnswer } = await request.json();\n\n    if (!username || !password) {\n      return json({ error: \"Username and password are required\" }, { status: 400 });\n    }\n\n    if (username.length < 3 || username.length > 15) {\n      return json({ error: \"Username must be between 3 and 15 characters\" }, { status: 400 });\n    }\n\n    if (!/^[a-zA-Z0-9_]+$/.test(username)) {\n      return json({ error: \"Username can only contain letters, numbers, and underscores\" }, { status: 400 });\n    }\n\n    if (password.length < PASSWORD_MIN_LENGTH) {\n      return json({ error: `Password must be at least ${PASSWORD_MIN_LENGTH} characters` }, { status: 400 });\n    }\n\n    if (!PASSWORD_REGEX.test(password)) {\n      return json({ error: \"Password must contain at least one uppercase letter, one lowercase letter, and one number\" }, { status: 400 });\n    }\n\n    if (email && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {\n      return json({ error: \"Invalid email format\" }, { status: 400 });\n    }\n\n    if (!captchaId || !captchaAnswer) {\n      return json({ error: \"Captcha verification is required\" }, { status: 400 });\n    }\n\n    const captchaValid = CaptchaService.validateCaptcha(captchaId, captchaAnswer, { consume: true });\n    if (!captchaValid) {\n      return json({ error: \"Invalid captcha. Please try again.\" }, { status: 400 });\n    }\n\n    const existingUser = await authService.findUserByIdentifier(email || username);\n    if (existingUser) {\n      if (email && existingUser.email === email) {\n        return json({ error: \"Email already registered\" }, { status: 400 });\n      }\n      if (existingUser.username === username) {\n        return json({ error: \"Username already taken\" }, { status: 400 });\n      }\n    }\n\n    const user = await authService.createUser(username, email, password);\n    const token = await authService.generateToken(user);\n    const isProduction = !dev;\n    const csrfToken = createCsrfToken();\n    const headers = new Headers();\n    headers.append(\"Set-Cookie\", createSessionCookie(token, isProduction));\n    headers.append(\"Set-Cookie\", createCsrfCookie(csrfToken, isProduction));\n\n    return json(user, {\n      headers,\n    });\n  } catch (error) {\n    return handleDatabaseError(error, \"register\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/auth/reset-password/request/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { authService } from \"$lib/server/services/auth\";\nimport { RateLimitService } from \"$lib/server/services/rate-limit\";\n\nexport async function POST({ request }: RequestEvent) {\n  const { identifier } = await request.json();\n\n  if (!identifier) {\n    return json({ error: \"Username or email is required\" }, { status: 400 });\n  }\n\n  const rateLimit = RateLimitService.checkPasswordResetLimit(identifier);\n  if (!rateLimit.allowed) {\n    return json({\n      error: `Too many reset attempts. Please try again in ${Math.ceil(rateLimit.timeLeft! / 60)} minutes.`\n    }, { status: 429 });\n  }\n\n  try {\n    const resetToken = await authService.createResetToken(identifier);\n    if (!resetToken) {\n      return json({\n        message: \"If an account exists with this username/email, password reset instructions will be sent\"\n      });\n    }\n\n    const user = await authService.findUserByIdentifier(identifier);\n    if (!user?.email) {\n      return json({\n        message: \"If an account exists with this username/email, password reset instructions will be sent\"\n      });\n    }\n\n    return json({\n      message: \"If an account exists with this username/email, password reset instructions will be sent\"\n    });\n  } catch (error) {\n    console.error(\"Password reset request error:\", error);\n    return json(\n      {\n        error: \"An error occurred while processing your request\"\n      },\n      { status: 500 },\n    );\n  }\n}\n"
  },
  {
    "path": "src/routes/api/auth/reset-password/reset/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { authService } from \"$lib/server/services/auth\";\nimport { RateLimitService } from \"$lib/server/services/rate-limit\";\n\nconst PASSWORD_MIN_LENGTH = 8;\nconst PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$/;\n\nexport async function POST({ request }: RequestEvent) {\n  const { token, newPassword } = await request.json();\n\n  if (!token || !newPassword) {\n    return json(\n      { error: \"Token and new password are required\" },\n      { status: 400 },\n    );\n  }\n\n  if (newPassword.length < PASSWORD_MIN_LENGTH) {\n    return json(\n      { error: `Password must be at least ${PASSWORD_MIN_LENGTH} characters long` },\n      { status: 400 },\n    );\n  }\n\n  if (!PASSWORD_REGEX.test(newPassword)) {\n    return json(\n      { error: \"Password must contain at least one uppercase letter, one lowercase letter, and one number\" },\n      { status: 400 },\n    );\n  }\n\n  const rateLimit = RateLimitService.checkPasswordResetLimit(token);\n  if (!rateLimit.allowed) {\n    return json({\n      error: `Too many attempts. Please try again in ${Math.ceil(rateLimit.timeLeft! / 60)} minutes.`\n    }, { status: 429 });\n  }\n\n  try {\n    const userId = await authService.validateResetToken(token);\n    if (!userId) {\n      return json(\n        { error: \"Invalid or expired reset link. Please request a new one.\" },\n        { status: 400 }\n      );\n    }\n\n    await authService.updatePassword(userId, newPassword);\n    await authService.clearResetToken(userId);\n\n    return json({ message: \"Your password has been successfully reset\" });\n  } catch (error) {\n    console.error(\"Password reset error:\", error);\n    return json(\n      {\n        error: \"An error occurred while resetting your password. Please try again.\"\n      },\n      { status: 500 },\n    );\n  }\n}\n"
  },
  {
    "path": "src/routes/api/captcha/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { CaptchaService } from \"$lib/server/services/captcha\";\nimport { z } from \"zod\";\n\nconst validateSchema = z.object({\n  id: z.string().length(32),\n  answer: z.string().min(1).max(10),\n});\n\n// Generate a new captcha\nexport async function GET({ getClientAddress }: RequestEvent) {\n  const { id, text } = CaptchaService.generateCaptcha();\n\n  // Return captcha ID and text for client-side rendering\n  // The text is only used for rendering the canvas image client-side\n  // Validation still happens server-side\n  return json({ id, text });\n}\n\n// Validate captcha (used during form submission)\nexport async function POST({ request }: RequestEvent) {\n  try {\n    const body = await request.json();\n    const validation = validateSchema.safeParse(body);\n\n    if (!validation.success) {\n      return json({ valid: false, error: \"Invalid request\" }, { status: 400 });\n    }\n\n    const { id, answer } = validation.data;\n    const isValid = CaptchaService.validateCaptcha(id, answer, { consume: false });\n\n    return json({ valid: isValid });\n  } catch {\n    return json({ valid: false, error: \"Validation failed\" }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/comments/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { getSession } from \"$lib/server/auth\";\nimport { validateComment } from \"$lib/shared/comment-validation\";\nimport { commentService } from \"$lib/server/services/comments\";\nimport { commentRateLimit } from \"$lib/server/services/rate-limit\";\nimport { handleDatabaseError } from \"$lib/server/services/db-error\";\nimport { z } from 'zod';\n\nconst mediaTypeSchema = z.enum(['movie', 'tv']);\nconst commentSchema = z.object({\n  mediaId: z.number().int().positive(),\n  mediaType: mediaTypeSchema,\n  content: z.string().min(1).max(1000),\n  parentId: z.number().int().positive().nullable().optional(),\n  season: z.number().int().min(1).max(100).optional(),\n  episode: z.number().int().min(1).max(2000).optional(),\n});\n\nfunction validateNumericInput(value: string | null, min: number, max: number): number | null {\n  if (!value) return null;\n  const num = parseInt(value);\n  if (isNaN(num) || num < min || num > max) return null;\n  return num;\n}\n\nfunction checkQueryComplexity(params: URLSearchParams): boolean {\n  const complexityScore =\n    (params.has('mediaId') ? 1 : 0) +\n    (params.has('mediaType') ? 1 : 0) +\n    (params.has('season') ? 2 : 0) +\n    (params.has('episode') ? 2 : 0);\n\n  return complexityScore <= 6;\n}\n\nexport async function GET({ url, cookies, getClientAddress }: RequestEvent) {\n  try {\n    const clientIp = getClientAddress();\n    if (!checkRateLimit(clientIp)) {\n      return json({ error: \"Rate limit exceeded\" }, { status: 429 });\n    }\n\n    if (!checkQueryComplexity(url.searchParams)) {\n      return json({ error: \"Query too complex\" }, { status: 400 });\n    }\n\n    const rawMediaId = url.searchParams.get(\"mediaId\");\n    const rawMediaType = url.searchParams.get(\"mediaType\");\n    const rawPage = url.searchParams.get(\"page\") || \"1\";\n    const rawLimit = url.searchParams.get(\"limit\") || \"10\";\n    const rawParentId = url.searchParams.get(\"parentId\");\n\n    const mediaId = validateNumericInput(rawMediaId, 1, Number.MAX_SAFE_INTEGER);\n    if (!mediaId) {\n      return json({ error: \"Invalid Media ID\" }, { status: 400 });\n    }\n\n    const mediaTypeResult = mediaTypeSchema.safeParse(rawMediaType);\n    if (!mediaTypeResult.success) {\n      return json({ error: \"Invalid media type\" }, { status: 400 });\n    }\n    const mediaType = mediaTypeResult.data;\n\n    const page = validateNumericInput(rawPage, 1, 1000) || 1;\n    const limit = validateNumericInput(rawLimit, 1, 100) || 10;\n    const parentId = validateNumericInput(rawParentId, 1, Number.MAX_SAFE_INTEGER);\n\n    const session = await getSession(cookies);\n    const userId = session?.userId;\n\n    const { comments, total } = await commentService.getComments(\n      mediaId,\n      mediaType,\n      userId,\n      parentId,\n      page,\n      limit\n    );\n\n    return json({\n      comments,\n      total,\n      page,\n      totalPages: Math.ceil(total / limit),\n    });\n  } catch (error) {\n    return handleDatabaseError(error, \"fetch comments\");\n  }\n}\n\nexport async function POST({ request, cookies, getClientAddress }: RequestEvent) {\n  try {\n    const clientIp = getClientAddress();\n    if (!checkRateLimit(clientIp)) {\n      return json({ error: \"Rate limit exceeded\" }, { status: 429 });\n    }\n\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    const body = await request.json();\n    const validationResult = commentSchema.safeParse(body);\n    if (!validationResult.success) {\n      return json({ error: \"Invalid request data\", details: validationResult.error }, { status: 400 });\n    }\n\n    const { content, mediaId, mediaType, parentId } = validationResult.data;\n\n    const contentValidation = validateComment(content);\n    if (!contentValidation.isValid) {\n      return json({ error: contentValidation.error || \"Invalid comment content\" }, { status: 400 });\n    }\n\n    const comment = await commentService.createComment({\n      userId: session.userId,\n      mediaId,\n      mediaType,\n      content,\n      parentId,\n    });\n\n    return json(comment);\n  } catch (error) {\n    return handleDatabaseError(error, \"create comment\");\n  }\n}\n\nexport async function DELETE({ url, cookies, getClientAddress }: RequestEvent) {\n  try {\n    const clientIp = getClientAddress();\n    if (!checkRateLimit(clientIp)) {\n      return json({ error: \"Rate limit exceeded\" }, { status: 429 });\n    }\n\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    const commentId = validateNumericInput(url.searchParams.get(\"id\"), 1, Number.MAX_SAFE_INTEGER);\n    if (!commentId) {\n      return json({ error: \"Invalid comment ID\" }, { status: 400 });\n    }\n\n    await commentService.deleteComment(commentId, session.userId);\n    return json({ success: true });\n  } catch (error) {\n    return handleDatabaseError(error, \"delete comment\");\n  }\n}\n\nfunction checkRateLimit(ip: string): boolean {\n  return commentRateLimit.checkRateLimit(ip);\n}\n"
  },
  {
    "path": "src/routes/api/comments/[id]/+server.ts",
    "content": "import { error, json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { prisma } from \"$lib/server/prisma\";\nimport { isDatabaseConnectionError } from \"$lib/server/services/db-error\";\n\nexport async function DELETE(event: RequestEvent) {\n  const user = event.locals.user;\n  if (!user) {\n    throw error(401, \"Unauthorized\");\n  }\n\n  const id = event.params.id;\n  if (!id) {\n    throw error(400, \"Comment ID is required\");\n  }\n\n  const commentId = parseInt(id);\n  if (isNaN(commentId) || commentId <= 0) {\n    throw error(400, \"Invalid comment ID\");\n  }\n\n  try {\n    const comment = await prisma.comment.findUnique({\n      where: { id: commentId }\n    });\n\n    if (!comment) {\n      throw error(404, \"Comment not found\");\n    }\n\n    // Clear authorization: user must own the comment OR be an admin\n    const isOwner = comment.userId === user.id;\n    if (!isOwner && !user.isAdmin) {\n      throw error(403, \"You can only delete your own comments\");\n    }\n\n    const updatedComment = await prisma.comment.update({\n      where: { id: commentId },\n      data: {\n        content: user.isAdmin && !isOwner ? \"[Comment removed by moderator]\" : \"[Comment deleted]\",\n        flagged: false,\n        flagReason: null,\n        flaggedAt: null\n      }\n    });\n\n    return json({ success: true, comment: updatedComment });\n  } catch (err) {\n    if (err && typeof err === 'object' && 'status' in err) {\n      throw err; // Re-throw SvelteKit errors\n    }\n    if (isDatabaseConnectionError(err)) {\n      console.error(\"Database unavailable:\", err instanceof Error ? err.message : 'Unknown error');\n      throw error(503, \"Service temporarily unavailable\");\n    }\n    console.error(\"Error deleting comment:\", err);\n    throw error(500, \"Failed to delete comment\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/comments/[id]/flag/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { prisma } from \"$lib/server/prisma\";\nimport { getSession } from \"$lib/server/auth\";\nimport { handleDatabaseError } from \"$lib/server/services/db-error\";\nimport { z } from \"zod\";\n\nconst flagSchema = z.object({\n  reason: z.string().max(500).optional(),\n});\n\nexport async function POST({ params, request, cookies }: RequestEvent) {\n  try {\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    const id = params.id;\n    if (!id) {\n      return json({ error: \"Comment ID is required\" }, { status: 400 });\n    }\n\n    const commentId = parseInt(id);\n    if (isNaN(commentId) || commentId <= 0) {\n      return json({ error: \"Invalid comment ID\" }, { status: 400 });\n    }\n\n    const body = await request.json();\n    const validation = flagSchema.safeParse(body);\n    if (!validation.success) {\n      return json({ error: \"Invalid flag reason\" }, { status: 400 });\n    }\n    const { reason } = validation.data;\n\n\n    const existingComment = await prisma.comment.findUnique({\n      where: { id: commentId }\n    });\n\n    if (!existingComment) {\n      return json({ error: \"Comment not found\" }, { status: 404 });\n    }\n\n    if (existingComment.flagged) {\n      return json({ error: \"Comment is already flagged\" }, { status: 400 });\n    }\n\n\n    const updatedComment = await prisma.comment.update({\n      where: { id: commentId },\n      data: {\n        flagged: true,\n        flagReason: reason || \"No reason provided\",\n        flaggedAt: new Date()\n      }\n    });\n\n    return json({\n      success: true,\n      comment: updatedComment\n    });\n  } catch (error) {\n    return handleDatabaseError(error, \"flag comment\");\n  }\n}\n\nexport async function DELETE({ params, cookies }: RequestEvent) {\n  try {\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    const user = await prisma.user.findUnique({\n      where: { id: session.userId }\n    });\n\n    if (!user?.isAdmin) {\n      return json({ error: \"Unauthorized\" }, { status: 403 });\n    }\n\n    const id = params.id;\n    if (!id) {\n      return json({ error: \"Comment ID is required\" }, { status: 400 });\n    }\n\n    const commentId = parseInt(id);\n    if (isNaN(commentId)) {\n      return json({ error: \"Invalid comment ID\" }, { status: 400 });\n    }\n\n\n    const updatedComment = await prisma.comment.update({\n      where: { id: commentId },\n      data: {\n        flagged: false,\n        flagReason: null,\n        flaggedAt: null\n      }\n    });\n\n    return json({\n      success: true,\n      comment: updatedComment\n    });\n  } catch (error) {\n    return handleDatabaseError(error, \"unflag comment\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/comments/[id]/unflag/+server.ts",
    "content": "import { error, json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { requireAdmin } from \"$lib/server/admin-middleware\";\nimport { prisma } from \"$lib/server/prisma\";\n\nexport async function POST(event: RequestEvent) {\n  await requireAdmin(event);\n\n  const id = event.params.id;\n  if (!id) {\n    throw error(400, \"Comment ID is required\");\n  }\n\n  const commentId = parseInt(id);\n  if (isNaN(commentId)) {\n    throw error(400, \"Invalid comment ID\");\n  }\n\n  try {\n    const existingComment = await prisma.comment.findUnique({\n      where: { id: commentId }\n    });\n\n    if (!existingComment) {\n      throw error(404, \"Comment not found\");\n    }\n\n    const updatedComment = await prisma.comment.update({\n      where: { id: commentId },\n      data: {\n        flagged: false,\n        flagReason: null,\n        flaggedAt: null\n      }\n    });\n\n    return json({\n      success: true,\n      comment: updatedComment\n    });\n  } catch (err) {\n    console.error(\"Error unflagging comment:\", err);\n    throw error(500, \"Failed to unflag comment\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/comments/flagged/+server.ts",
    "content": "import { error, json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { CommentService } from \"$lib/services/comments\";\nimport { requireAdmin } from \"$lib/server/admin-middleware\";\n\nconst commentService = new CommentService();\n\nexport async function GET(event: RequestEvent) {\n\n  await requireAdmin(event);\n\n  const page = parseInt(event.url.searchParams.get(\"page\") || \"1\");\n  const limit = parseInt(event.url.searchParams.get(\"limit\") || \"10\");\n\n  try {\n    const { comments, total } = await commentService.getFlaggedComments(\n      page,\n      limit,\n    );\n\n    return json({\n      comments,\n      total,\n      page,\n      totalPages: Math.ceil(total / limit)\n    });\n  } catch (err) {\n    console.error(\"Error fetching flagged comments:\", err);\n    throw error(500, \"Failed to fetch flagged comments\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/comments/like/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { prisma } from \"$lib/server/prisma\";\nimport { getSession } from \"$lib/server/auth\";\nimport { RateLimitService } from \"$lib/server/services/rate-limit\";\nimport { handleDatabaseError } from \"$lib/server/services/db-error\";\nimport { z } from \"zod\";\n\nconst likeSchema = z.object({\n  commentId: z.number().int().positive(),\n});\n\nexport async function POST({ request, cookies }: RequestEvent) {\n  try {\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    // Rate limit likes to prevent spam\n    const rateLimit = RateLimitService.checkLikeLimit(session.userId);\n    if (!rateLimit.allowed) {\n      return json(\n        { error: `Too many likes. Please try again in ${rateLimit.timeLeft} seconds.` },\n        { status: 429 }\n      );\n    }\n\n    const body = await request.json();\n    const validation = likeSchema.safeParse(body);\n    if (!validation.success) {\n      return json({ error: \"Invalid comment ID\" }, { status: 400 });\n    }\n    const { commentId } = validation.data;\n\n\n    const comment = await prisma.comment.findUnique({\n      where: { id: commentId },\n    });\n\n    if (!comment) {\n      return json({ error: \"Comment not found\" }, { status: 404 });\n    }\n\n\n    const existingLike = await prisma.commentLike.findUnique({\n      where: {\n        userId_commentId: {\n          userId: session.userId,\n          commentId,\n        },\n      },\n    });\n\n    if (existingLike) {\n\n      await prisma.commentLike.delete({\n        where: {\n          userId_commentId: {\n            userId: session.userId,\n            commentId,\n          },\n        },\n      });\n    } else {\n\n      await prisma.commentLike.create({\n        data: {\n          userId: session.userId,\n          commentId,\n        },\n      });\n    }\n\n\n    const updatedComment = await prisma.comment.findUnique({\n      where: { id: commentId },\n      include: {\n        _count: {\n          select: {\n            likes: true,\n          },\n        },\n      },\n    });\n\n    return json({\n      isLiked: !existingLike,\n      likeCount: updatedComment?._count.likes || 0,\n    });\n  } catch (err) {\n    return handleDatabaseError(err, \"toggle like\");\n  }\n}\n\nexport async function GET({ url, cookies }: RequestEvent) {\n  try {\n    const session = await getSession(cookies);\n    if (!session?.userId) {\n      return json({ error: \"Unauthorized\" }, { status: 401 });\n    }\n\n    const rawCommentId = url.searchParams.get(\"commentId\");\n    if (!rawCommentId) {\n      return json({ error: \"Comment ID is required\" }, { status: 400 });\n    }\n\n    const commentId = parseInt(rawCommentId, 10);\n    if (isNaN(commentId) || commentId <= 0) {\n      return json({ error: \"Invalid comment ID\" }, { status: 400 });\n    }\n\n    const like = await prisma.commentLike.findUnique({\n      where: {\n        userId_commentId: {\n          userId: session.userId,\n          commentId,\n        },\n      },\n    });\n\n    const likeCount = await prisma.commentLike.count({\n      where: {\n        commentId,\n      },\n    });\n\n    return json({\n      isLiked: !!like,\n      likeCount,\n    });\n  } catch (err) {\n    return handleDatabaseError(err, \"check like status\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/image/[...path]/+server.ts",
    "content": "import { error } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_IMAGE_URL } from \"$env/static/private\";\n\n// Allowed image sizes from TMDB\nconst ALLOWED_SIZES = new Set([\n  'w45', 'w92', 'w154', 'w185', 'w300', 'w342', 'w500', 'w780',\n  'w1280', 'h632', 'original'\n]);\n\n// Image path pattern: alphanumeric, underscore, hyphen, and dots only\nconst PATH_PATTERN = /^[a-zA-Z0-9_\\-./]+$/;\n\nexport async function GET({ params, fetch }: RequestEvent) {\n  if (!TMDB_IMAGE_URL) {\n    throw error(500, \"TMDB image URL not configured\");\n  }\n\n  const path = params.path;\n  if (!path) {\n    throw error(400, \"Image path is required\");\n  }\n\n  const [size, ...imagePath] = path.split(\"/\");\n  const actualPath = imagePath.join(\"/\");\n\n  // Validate size against allowlist\n  if (!ALLOWED_SIZES.has(size)) {\n    throw error(400, \"Invalid image size\");\n  }\n\n  if (!actualPath) {\n    throw error(400, \"Invalid image path\");\n  }\n\n  // Sanitize path - prevent path traversal\n  if (actualPath.includes('..') || !PATH_PATTERN.test(actualPath)) {\n    throw error(400, \"Invalid image path\");\n  }\n\n  try {\n    const imageUrl = `${TMDB_IMAGE_URL}/${size}${actualPath.startsWith(\"/\") ? actualPath : \"/\" + actualPath}`;\n    const response = await fetch(imageUrl);\n\n    if (!response.ok) {\n      throw error(response.status, \"Failed to fetch image\");\n    }\n\n    const contentType = response.headers.get(\"Content-Type\");\n    const headers = new Headers();\n    headers.set(\"Content-Type\", contentType || \"image/jpeg\");\n    headers.set(\"Cache-Control\", \"public, max-age=31536000\");\n\n    return new Response(response.body, {\n      status: 200,\n      headers,\n    });\n  } catch (err) {\n    if (err && typeof err === 'object' && 'status' in err) {\n      throw err; // Re-throw SvelteKit errors\n    }\n    console.error(\"Image proxy error:\", err);\n    throw error(500, \"Failed to load image\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/movies/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\nimport type { TMDBResponse, TMDBMovie } from \"$lib/types/tmdb\";\n\nexport async function GET({ fetch, url }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      results: [],\n      total_pages: 0,\n      total_results: 0,\n      page: 1,\n      error: \"TMDB API is not configured. Please add TMDB_API_KEY to your .env file.\"\n    }, { status: 200 });\n  }\n\n  const page = url.searchParams.get(\"page\") || \"1\";\n  const sort = url.searchParams.get(\"sort\") || \"trending\";\n  const genre = url.searchParams.get(\"genre\");\n  const year = url.searchParams.get(\"year\");\n\n  try {\n    let apiUrl: string;\n    const baseParams = `api_key=${TMDB_API_KEY}&language=en-US&page=${page}&vote_average.gte=0.1`;\n\n    switch (sort) {\n      case \"trending\":\n        apiUrl = `${TMDB_API_URL}/trending/movie/week?${baseParams}`;\n        break;\n      case \"popular\":\n        apiUrl = `${TMDB_API_URL}/movie/popular?${baseParams}`;\n        break;\n      case \"top_rated\":\n        apiUrl = `${TMDB_API_URL}/movie/top_rated?${baseParams}`;\n        break;\n      case \"now_playing\":\n        apiUrl = `${TMDB_API_URL}/movie/now_playing?${baseParams}`;\n        break;\n      case \"upcoming\":\n        apiUrl = `${TMDB_API_URL}/movie/upcoming?${baseParams}`;\n        break;\n      default:\n        apiUrl = `${TMDB_API_URL}/discover/movie?${baseParams}`;\n    }\n\n    if (genre || year) {\n      apiUrl = `${TMDB_API_URL}/discover/movie?${baseParams}`;\n      if (genre) apiUrl += `&with_genres=${genre}`;\n      if (year) apiUrl += `&primary_release_year=${year}`;\n    }\n\n    const response = await fetch(apiUrl);\n\n    if (response.status === 401) {\n      return json({\n        results: [],\n        total_pages: 0,\n        total_results: 0,\n        page: 1,\n        error: \"Invalid TMDB API key. Please check your TMDB_API_KEY in .env file.\"\n      }, { status: 200 });\n    }\n\n    if (!response.ok) {\n      return json({\n        results: [],\n        total_pages: 0,\n        total_results: 0,\n        page: 1,\n        error: `Failed to fetch movies (${response.status})`\n      }, { status: 200 });\n    }\n\n    const data = await response.json() as TMDBResponse<TMDBMovie>;\n    data.results = data.results.filter((movie) => movie.vote_average > 0);\n\n    return json(data);\n  } catch (err) {\n    console.error(\"Error fetching movies:\", err);\n    return json({\n      results: [],\n      total_pages: 0,\n      total_results: 0,\n      page: 1,\n      error: \"Failed to fetch movies. Please try again later.\"\n    }, { status: 200 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/movies/trending/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\n\nexport async function GET({ fetch }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      results: [],\n      error: \"TMDB API is not configured\"\n    }, { status: 200 });\n  }\n\n  try {\n    const response = await fetch(\n      `${TMDB_API_URL}/trending/movie/week?api_key=${TMDB_API_KEY}&language=en-US`,\n    );\n\n    if (!response.ok) {\n      return json({\n        results: [],\n        error: `Failed to fetch trending movies (${response.status})`\n      }, { status: 200 });\n    }\n\n    const data = await response.json();\n    return json(data);\n  } catch (err) {\n    console.error(\"Error fetching trending movies:\", err);\n    return json({\n      results: [],\n      error: \"Failed to fetch trending movies\"\n    }, { status: 200 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/providers/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport { env } from \"$env/dynamic/private\";\n\n// Validate that URLs are HTTPS and from expected patterns\nfunction validateProviderUrl(url: string | undefined): string | null {\n  if (!url) return null;\n\n  try {\n    const parsed = new URL(url);\n    // Only allow HTTPS URLs\n    if (parsed.protocol !== 'https:') {\n      return null;\n    }\n    return url;\n  } catch {\n    return null;\n  }\n}\n\nexport async function GET() {\n  return json({\n    vidsrc: validateProviderUrl(env.VIDSRC_BASE_URL),\n    vidlink: validateProviderUrl(env.VIDLINK_BASE_URL),\n    movies111: validateProviderUrl(env.MOVIES111_BASE_URL),\n    embed2: validateProviderUrl(env.EMBED2_BASE_URL),\n  });\n}\n"
  },
  {
    "path": "src/routes/api/release-info/[type]/[id]/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\n\ninterface ReleaseDate {\n  certification: string;\n  iso_639_1?: string;\n  release_date: string;\n  type: number;\n  note?: string;\n}\n\ninterface ReleaseDateResult {\n  iso_3166_1: string;\n  release_dates: ReleaseDate[];\n}\n\ninterface WatchProviderRegion {\n  flatrate?: Array<{ provider_name: string }>;\n  rent?: Array<{ provider_name: string }>;\n  buy?: Array<{ provider_name: string }>;\n}\n\nexport async function GET({ params, fetch }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      releaseType: \"Unknown Quality\",\n      certifications: {},\n    });\n  }\n\n  const { type, id } = params;\n  const mediaType = type === \"tv\" ? \"tv\" : \"movie\";\n\n  try {\n    const [releaseDatesResponse, watchProvidersResponse] = await Promise.all([\n      fetch(`${TMDB_API_URL}/${mediaType}/${id}/release_dates?api_key=${TMDB_API_KEY}`),\n      fetch(`${TMDB_API_URL}/${mediaType}/${id}/watch/providers?api_key=${TMDB_API_KEY}`),\n    ]);\n\n    if (!releaseDatesResponse.ok || !watchProvidersResponse.ok) {\n      return json({\n        releaseType: \"Unknown Quality\",\n        certifications: {},\n      });\n    }\n\n    const releaseDatesData = await releaseDatesResponse.json();\n    const watchProvidersData = await watchProvidersResponse.json();\n\n    const currentUtcDate = new Date(\n      Date.UTC(\n        new Date().getUTCFullYear(),\n        new Date().getUTCMonth(),\n        new Date().getUTCDate(),\n      ),\n    );\n\n    const releases: ReleaseDate[] = releaseDatesData.results?.flatMap(\n      (result: ReleaseDateResult) => result.release_dates,\n    ) || [];\n\n    // Extract certifications\n    const certifications: Record<string, string> = {};\n    releaseDatesData.results?.forEach((result: ReleaseDateResult) => {\n      const certificationEntry = result.release_dates.find(\n        (release) => release.certification,\n      );\n      if (certificationEntry) {\n        certifications[result.iso_3166_1] = certificationEntry.certification;\n      }\n    });\n\n    // Check release types\n    const isDigitalRelease = releases.some(\n      (release) =>\n        [4, 6].includes(release.type) &&\n        new Date(release.release_date).getTime() <= currentUtcDate.getTime(),\n    );\n\n    const isInTheaters = releases.some((release) => {\n      const releaseDate = new Date(release.release_date);\n      return release.type === 3 && releaseDate.getTime() <= currentUtcDate.getTime();\n    });\n\n    const hasFutureRelease = releases.some(\n      (release) => new Date(release.release_date).getTime() > currentUtcDate.getTime(),\n    );\n\n    const availableRegions = Object.keys(watchProvidersData.results || {});\n    const isStreamingAvailable = availableRegions.some(\n      (region) => (watchProvidersData.results?.[region]?.flatrate || []).length > 0,\n    );\n\n    const isRentalOrPurchaseAvailable = availableRegions.some((region: string) => {\n      const data = watchProvidersData.results?.[region] as WatchProviderRegion | undefined;\n      const rentProviders = data?.rent || [];\n      const buyProviders = data?.buy || [];\n      return rentProviders.length > 0 || buyProviders.length > 0;\n    });\n\n    // Determine release type\n    let releaseType: string;\n    if (isInTheaters && !isStreamingAvailable && !isDigitalRelease) {\n      releaseType = \"Cam\";\n    } else if (isStreamingAvailable || isDigitalRelease) {\n      releaseType = \"HD\";\n    } else if (hasFutureRelease && !isInTheaters) {\n      releaseType = \"Not Released Yet\";\n    } else if (isRentalOrPurchaseAvailable) {\n      releaseType = \"Rental/Buy Available\";\n    } else {\n      releaseType = \"Unknown Quality\";\n    }\n\n    return json({ releaseType, certifications });\n  } catch (err) {\n    console.error(\"Error fetching release info:\", err);\n    return json({\n      releaseType: \"Unknown Quality\",\n      certifications: {},\n    });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/tv/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\nimport type { TMDBResponse, TMDBTVShow } from \"$lib/types/tmdb\";\n\nexport async function GET({ fetch, url }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      results: [],\n      total_pages: 0,\n      total_results: 0,\n      page: 1,\n      error: \"TMDB API is not configured. Please add TMDB_API_KEY to your .env file.\"\n    }, { status: 200 });\n  }\n\n  const page = url.searchParams.get(\"page\") || \"1\";\n  const sort = url.searchParams.get(\"sort\") || \"trending\";\n  const genre = url.searchParams.get(\"genre\");\n  const year = url.searchParams.get(\"year\");\n\n  try {\n    let apiUrl: string;\n    const baseParams = `api_key=${TMDB_API_KEY}&language=en-US&page=${page}&vote_average.gte=0.1`;\n\n    switch (sort) {\n      case \"trending\":\n        apiUrl = `${TMDB_API_URL}/trending/tv/week?${baseParams}`;\n        break;\n      case \"popular\":\n        apiUrl = `${TMDB_API_URL}/tv/popular?${baseParams}`;\n        break;\n      case \"top_rated\":\n        apiUrl = `${TMDB_API_URL}/tv/top_rated?${baseParams}`;\n        break;\n      case \"on_the_air\":\n        apiUrl = `${TMDB_API_URL}/tv/on_the_air?${baseParams}`;\n        break;\n      case \"airing_today\":\n        apiUrl = `${TMDB_API_URL}/tv/airing_today?${baseParams}`;\n        break;\n      default:\n        apiUrl = `${TMDB_API_URL}/discover/tv?${baseParams}`;\n    }\n\n    if (genre || year) {\n      apiUrl = `${TMDB_API_URL}/discover/tv?${baseParams}`;\n      if (genre) apiUrl += `&with_genres=${genre}`;\n      if (year) apiUrl += `&first_air_date_year=${year}`;\n    }\n\n    const response = await fetch(apiUrl);\n\n    if (response.status === 401) {\n      return json({\n        results: [],\n        total_pages: 0,\n        total_results: 0,\n        page: 1,\n        error: \"Invalid TMDB API key. Please check your TMDB_API_KEY in .env file.\"\n      }, { status: 200 });\n    }\n\n    if (!response.ok) {\n      return json({\n        results: [],\n        total_pages: 0,\n        total_results: 0,\n        page: 1,\n        error: `Failed to fetch TV shows (${response.status})`\n      }, { status: 200 });\n    }\n\n    const data = await response.json() as TMDBResponse<TMDBTVShow>;\n    data.results = data.results.filter((show) => show.vote_average > 0);\n\n    return json(data);\n  } catch (err) {\n    console.error(\"Error fetching TV shows:\", err);\n    return json({\n      results: [],\n      total_pages: 0,\n      total_results: 0,\n      page: 1,\n      error: \"Failed to fetch TV shows. Please try again later.\"\n    }, { status: 200 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/tv/[id]/season/[season]/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\n\ninterface Episode {\n  id: number;\n  name: string;\n  overview: string;\n  episode_number: number;\n  air_date: string;\n  still_path: string | null;\n  vote_average: number;\n  vote_count: number;\n  runtime: number | null;\n}\n\ninterface SeasonResponse {\n  episodes: Episode[];\n}\n\nexport async function GET({ params, fetch }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      episodes: [],\n      error: \"TMDB API is not configured\"\n    }, { status: 200 });\n  }\n\n  const { id, season } = params;\n\n  try {\n    const response = await fetch(\n      `${TMDB_API_URL}/tv/${id}/season/${season}?api_key=${TMDB_API_KEY}&language=en-US`,\n    );\n\n    if (!response.ok) {\n      return json({\n        episodes: [],\n        error: `Failed to fetch season details (${response.status})`\n      }, { status: 200 });\n    }\n\n    const data: SeasonResponse = await response.json();\n\n    const currentDate = new Date();\n    const episodes = data.episodes.filter((episode: Episode) => {\n      const airDate = new Date(episode.air_date);\n      return airDate <= currentDate;\n    });\n\n    return json({ episodes });\n  } catch (err) {\n    console.error(\"Error fetching season episodes:\", err);\n    return json({\n      episodes: [],\n      error: \"Failed to fetch season episodes\"\n    }, { status: 200 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/tv/[id]/seasons/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\n\nexport async function GET({ params, fetch }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      seasons: [],\n      error: \"TMDB API is not configured\"\n    }, { status: 200 });\n  }\n\n  const { id } = params;\n\n  try {\n    const response = await fetch(\n      `${TMDB_API_URL}/tv/${id}?api_key=${TMDB_API_KEY}&language=en-US`,\n    );\n\n    if (!response.ok) {\n      return json({\n        seasons: [],\n        error: `Failed to fetch TV show details (${response.status})`\n      }, { status: 200 });\n    }\n\n    const data = await response.json();\n    return json({ seasons: data.seasons });\n  } catch (err) {\n    console.error(\"Error fetching TV show seasons:\", err);\n    return json({\n      seasons: [],\n      error: \"Failed to fetch TV show seasons\"\n    }, { status: 200 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/tv/trending/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { TMDB_API_KEY, TMDB_API_URL } from \"$env/static/private\";\n\nexport async function GET({ fetch }: RequestEvent) {\n  if (!TMDB_API_KEY || !TMDB_API_URL) {\n    return json({\n      results: [],\n      error: \"TMDB API is not configured\"\n    }, { status: 200 });\n  }\n\n  try {\n    const response = await fetch(\n      `${TMDB_API_URL}/trending/tv/week?api_key=${TMDB_API_KEY}&language=en-US`,\n    );\n\n    if (!response.ok) {\n      return json({\n        results: [],\n        error: `Failed to fetch trending TV shows (${response.status})`\n      }, { status: 200 });\n    }\n\n    const data = await response.json();\n    return json(data);\n  } catch (err) {\n    console.error(\"Error fetching trending TV shows:\", err);\n    return json({\n      results: [],\n      error: \"Failed to fetch trending TV shows\"\n    }, { status: 200 });\n  }\n}\n"
  },
  {
    "path": "src/routes/api/users/search/+server.ts",
    "content": "import { error, json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { prisma } from \"$lib/server/prisma\";\n\nfunction escapeLikePattern(pattern: string): string {\n  return pattern.replace(/[%_\\\\]/g, '\\\\$&');\n}\n\nexport async function GET({ url, locals }: RequestEvent) {\n  if (!locals.user) {\n    throw error(401, \"Unauthorized\");\n  }\n\n  const query = url.searchParams.get(\"q\");\n  if (!query) {\n    throw error(400, \"Query parameter is required\");\n  }\n\n  if (query.length > 50) {\n    throw error(400, \"Query too long\");\n  }\n\n  try {\n    const escapedQuery = escapeLikePattern(query);\n    const users = await prisma.user.findMany({\n      where: {\n        username: {\n          contains: escapedQuery,\n        },\n        NOT: {\n          id: locals.user.id,\n        },\n      },\n      select: {\n        id: true,\n        username: true,\n      },\n      orderBy: {\n        username: 'asc',\n      },\n      take: 5,\n    });\n\n    return json(users);\n  } catch (err) {\n    console.error(\"Error searching users:\", err);\n    throw error(500, \"Failed to search users\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/watchlist/+server.ts",
    "content": "import { json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { watchlistService } from \"$lib/server/services/watchlist\";\nimport { handleDatabaseError } from \"$lib/server/services/db-error\";\nimport { z } from \"zod\";\n\nconst mediaTypeSchema = z.enum([\"movie\", \"tv\"]);\n\nconst addToWatchlistSchema = z.object({\n  mediaId: z.number().int().positive(),\n  mediaType: mediaTypeSchema,\n  title: z.string().min(1).max(500),\n  posterPath: z.string().max(500).nullable().optional(),\n  voteAverage: z.number().min(0).max(10).optional().default(0),\n});\n\nconst removeFromWatchlistSchema = z.object({\n  mediaId: z.number().int().positive(),\n  mediaType: mediaTypeSchema,\n});\n\nexport async function GET({ locals }: RequestEvent) {\n  if (!locals.user) {\n    return json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  try {\n    const items = await watchlistService.getWatchlist(locals.user.id);\n    const total = await watchlistService.getWatchlistCount(locals.user.id);\n    return json({ items, total });\n  } catch (err) {\n    return handleDatabaseError(err, \"fetch watchlist\");\n  }\n}\n\nexport async function POST({ request, locals }: RequestEvent) {\n  if (!locals.user) {\n    return json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  try {\n    const body = await request.json();\n    const validation = addToWatchlistSchema.safeParse(body);\n\n    if (!validation.success) {\n      return json(\n        { error: \"Invalid request data\", details: validation.error.flatten() },\n        { status: 400 },\n      );\n    }\n\n    const { mediaId, mediaType, title, posterPath, voteAverage } = validation.data;\n\n    const watchlistItem = await watchlistService.addToWatchlist(\n      locals.user.id,\n      mediaId,\n      mediaType,\n      title,\n      posterPath ?? null,\n      voteAverage,\n    );\n\n    return json(watchlistItem);\n  } catch (err) {\n    return handleDatabaseError(err, \"add to watchlist\");\n  }\n}\n\nexport async function DELETE({ request, locals }: RequestEvent) {\n  if (!locals.user) {\n    return json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  try {\n    const body = await request.json();\n    const validation = removeFromWatchlistSchema.safeParse(body);\n\n    if (!validation.success) {\n      return json(\n        { error: \"Invalid request data\", details: validation.error.flatten() },\n        { status: 400 },\n      );\n    }\n\n    const { mediaId, mediaType } = validation.data;\n\n    await watchlistService.removeFromWatchlist(\n      locals.user.id,\n      mediaId,\n      mediaType,\n    );\n\n    return json({ message: \"Item removed from watchlist\" });\n  } catch (err) {\n    return handleDatabaseError(err, \"remove from watchlist\");\n  }\n}\n"
  },
  {
    "path": "src/routes/api/watchlist/check/+server.ts",
    "content": "import { error, json } from \"@sveltejs/kit\";\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { watchlistService } from \"$lib/server/services/watchlist\";\nimport { isDatabaseConnectionError } from \"$lib/server/services/db-error\";\n\nexport async function GET({ url, locals }: RequestEvent) {\n  if (!locals.user) {\n    throw error(401, \"Unauthorized\");\n  }\n\n  const mediaId = parseInt(url.searchParams.get(\"mediaId\") || \"\");\n  const mediaType = url.searchParams.get(\"mediaType\");\n\n  if (isNaN(mediaId) || !mediaType || ![\"movie\", \"tv\"].includes(mediaType)) {\n    throw error(400, \"Invalid mediaId or mediaType\");\n  }\n\n  try {\n    const inWatchlist = await watchlistService.isInWatchlist(\n      locals.user.id,\n      mediaId,\n      mediaType as \"movie\" | \"tv\"\n    );\n\n    return json({ inWatchlist });\n  } catch (err) {\n    if (isDatabaseConnectionError(err)) {\n      console.error(\"Database unavailable:\", err instanceof Error ? err.message : 'Unknown error');\n      throw error(503, \"Service temporarily unavailable\");\n    }\n    console.error(\"Error checking watchlist:\", err);\n    throw error(500, \"Failed to check watchlist\");\n  }\n}\n"
  },
  {
    "path": "src/routes/dmca/+page.svelte",
    "content": "<div class=\"container mx-auto px-4 py-8\">\n  <div class=\"max-w-4xl mx-auto\">\n    <h1 class=\"text-4xl font-bold mb-8\">DMCA Notice</h1>\n\n    <div class=\"prose prose-invert\">\n      <div class=\"bg-gray-800 p-6 rounded-lg mb-8\">\n        <p class=\"text-lg font-semibold text-white\">\n          This site does not store any files on our server, we only linked to the media which is hosted on 3rd party services.\n        </p>\n      </div>\n\n      <h2 class=\"text-2xl font-semibold mt-8 mb-4\">About Our Service</h2>\n      <p class=\"mb-6\">\n        This site is a search and indexing service that:\n      </p>\n      <ul class=\"list-disc pl-6 mb-6\">\n        <li>Provides links to content hosted on third-party services</li>\n        <li>Does not host or upload any media files</li>\n        <li>Does not have control over third-party content</li>\n        <li>Functions as a search engine for streaming content</li>\n      </ul>\n\n      <h2 class=\"text-2xl font-semibold mt-8 mb-4\">Third-Party Content</h2>\n      <p class=\"mb-6\">\n        All media content displayed on this site is hosted by and streamed from third-party services. We:\n      </p>\n      <ul class=\"list-disc pl-6 mb-6\">\n        <li>Do not upload or host any media files</li>\n        <li>Do not control the content on third-party services</li>\n        <li>Only provide indexing and search functionality</li>\n        <li>Link to publicly available content from various sources</li>\n      </ul>\n\n      <h2 class=\"text-2xl font-semibold mt-8 mb-4\">Copyright Claims</h2>\n      <p class=\"mb-6\">\n        If you find content that infringes your copyright:\n      </p>\n      <ul class=\"list-disc pl-6 mb-6\">\n        <li>Contact the hosting service directly where the content is stored</li>\n        <li>Request removal from the source hosting the content</li>\n        <li>Once removed from the source, it will no longer be accessible through our service</li>\n      </ul>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/routes/login/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { authStore } from '$lib/stores/auth';\n  import { goto } from '$app/navigation';\n\n  let identifier = '';\n  let password = '';\n  let loading = false;\n  let error = '';\n\n  async function handleSubmit() {\n    if (!identifier || !password) {\n      error = 'Please fill in all fields';\n      return;\n    }\n\n    loading = true;\n    error = '';\n\n    try {\n      const success = await authStore.login(identifier, password);\n      if (success) {\n        goto('/');\n      } else {\n        error = 'Invalid username/email or password';\n      }\n    } catch (err) {\n      error = 'An error occurred during login';\n      console.error('Login error:', err);\n    } finally {\n      loading = false;\n    }\n  }\n</script>\n\n<div class=\"min-h-[calc(100vh-16rem)] flex items-center justify-center px-4\">\n  <div class=\"w-full max-w-md\">\n    <div class=\"bg-gray-800 rounded-lg shadow-xl p-8\">\n      <h1 class=\"text-3xl font-bold text-center mb-8\">Login</h1>\n\n      <form on:submit|preventDefault={handleSubmit} class=\"space-y-6\">\n        {#if error}\n          <div class=\"bg-red-500/10 border border-red-500 text-red-500 px-4 py-3 rounded\">\n            {error}\n          </div>\n        {/if}\n\n        <div>\n          <label for=\"identifier\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n            Username or Email\n          </label>\n          <input\n            type=\"text\"\n            id=\"identifier\"\n            bind:value={identifier}\n            class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n            required\n            placeholder=\"Enter your username or email\"\n            autocomplete=\"username\"\n          />\n        </div>\n\n        <div>\n          <label for=\"password\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n            Password\n          </label>\n          <input\n            type=\"password\"\n            id=\"password\"\n            bind:value={password}\n            class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n            required\n            placeholder=\"Enter your password\"\n            autocomplete=\"current-password\"\n          />\n        </div>\n\n        <div class=\"flex items-center justify-between\">\n          <a href=\"/reset-password\" class=\"text-sm text-primary-400 hover:text-primary-300\">\n            Forgot password?\n          </a>\n        </div>\n\n        <button\n          type=\"submit\"\n          class=\"w-full py-3 px-4 bg-primary-500 text-white rounded-lg font-medium hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed\"\n          disabled={loading}\n        >\n          {loading ? 'Logging in...' : 'Login'}\n        </button>\n\n        <div class=\"text-center text-sm text-gray-400\">\n          Don't have an account?\n          <a href=\"/register\" class=\"text-primary-400 hover:text-primary-300\">\n            Register\n          </a>\n        </div>\n      </form>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/routes/media/[id]/+page.server.ts",
    "content": "import { error } from \"@sveltejs/kit\";\nimport type { ServerLoad } from \"@sveltejs/kit\";\nimport { TMDBService } from \"$lib/services/tmdb\";\nimport type { TMDBMovie, TMDBTVShow } from \"$lib/types/tmdb\";\n\ninterface PageData {\n  media: TMDBMovie | TMDBTVShow;\n  type: \"movie\" | \"tv\";\n  season?: number;\n  episode?: number;\n}\n\nexport const load: ServerLoad = async ({ params, url }) => {\n  const tmdb = new TMDBService();\n  const mediaType = url.searchParams.get(\"type\") || \"movie\";\n  const season = url.searchParams.get(\"season\");\n  const episode = url.searchParams.get(\"episode\");\n  const id = parseInt(params.id as string);\n\n  if (isNaN(id)) {\n    throw error(400, \"Invalid media ID\");\n  }\n\n  try {\n    if (mediaType === \"movie\") {\n      const movie = await tmdb.getMovieDetails(id);\n      return {\n        media: movie,\n        type: \"movie\",\n      } satisfies PageData;\n    } else {\n      const show = await tmdb.getTVShowDetails(id);\n      return {\n        media: show,\n        type: \"tv\",\n        season: season ? parseInt(season) : 1,\n        episode: episode ? parseInt(episode) : 1,\n      } satisfies PageData;\n    }\n  } catch (e) {\n    console.error(\"Failed to load media details:\", e);\n    throw error(500, \"Failed to load media details\");\n  }\n};\n"
  },
  {
    "path": "src/routes/media/[id]/+page.svelte",
    "content": "<script lang=\"ts\">\n  import VideoPlayer from '$lib/components/VideoPlayer.svelte';\n  import CommentList from '$lib/components/CommentList.svelte';\n  import WatchlistButton from '$lib/components/WatchlistButton.svelte';\n  import EpisodeSelector from '$lib/components/EpisodeSelector.svelte';\n  import NextEpisode from '$lib/components/NextEpisode.svelte';\n  import { providers, getDefaultProvider } from '$lib/services/providers';\n  import { onMount } from 'svelte';\n  import { goto } from '$app/navigation';\n  import { page } from '$app/stores';\n\n  interface PageData {\n    media: {\n      id: number;\n      title?: string;\n      name?: string;\n      backdrop_path?: string;\n      poster_path?: string | null;\n      release_date?: string;\n      first_air_date?: string;\n      runtime?: number;\n      vote_average: number;\n      overview: string;\n      genres?: Array<{ id: number; name: string }>;\n      number_of_seasons?: number;\n      number_of_episodes?: number;\n    };\n    type: 'movie' | 'tv';\n    season?: number;\n    episode?: number;\n  }\n\n  export let data: PageData;\n  $: media = data.media;\n  $: mediaType = data.type;\n  $: selectedSeason = data.season;\n  $: selectedEpisode = data.episode;\n  let showEpisodeModal = false;\n  let videoPlayer: VideoPlayer;\n  let selectedProviderId = getDefaultProvider().id;\n\n  $: seasonCount = mediaType === 'tv' ? media?.number_of_seasons : 0;\n  $: episodeCount = mediaType === 'tv' ? media?.number_of_episodes : 0;\n\n  function handleEpisodeSelect(event: CustomEvent<{ season: number; episode: number }>) {\n    const { season, episode } = event.detail;\n    const url = new URL($page.url);\n    url.searchParams.set('season', season.toString());\n    url.searchParams.set('episode', episode.toString());\n    goto(url.toString(), { replaceState: true });\n  }\n\n  function handleNextEpisode(season: number, episode: number) {\n    const url = new URL($page.url);\n    url.searchParams.set('season', season.toString());\n    url.searchParams.set('episode', episode.toString());\n    goto(url.toString(), { replaceState: true });\n  }\n\n  function handleProviderChange(event: Event) {\n    const select = event.target as HTMLSelectElement;\n    selectedProviderId = select.value;\n    videoPlayer?.changeProvider(select.value);\n  }\n\n  function formatDate(dateString: string | undefined): string {\n    if (!dateString) return '';\n    return new Date(dateString).toLocaleDateString('en-US', {\n      year: 'numeric',\n      month: 'long',\n      day: 'numeric'\n    });\n  }\n</script>\n\n<div class=\"min-h-screen bg-gray-900\">\n  <div class=\"relative\">\n    {#if media?.backdrop_path}\n      <div class=\"absolute inset-0 h-[50vh]\">\n        <img\n          src={`/api/image/original${media.backdrop_path}`}\n          alt={media.title || media.name || ''}\n          class=\"w-full h-full object-cover\"\n        />\n        <div class=\"absolute inset-0 bg-gradient-to-b from-gray-900/50 via-gray-900/80 to-gray-900\"></div>\n      </div>\n    {/if}\n\n    <div class=\"relative container mx-auto px-4 pt-4 lg:pt-[20vh]\">\n      <div class=\"flex flex-col lg:flex-row gap-8\">\n        <!-- Right Column: Content (Moved up for mobile) -->\n        <div class=\"w-full lg:w-3/4 lg:order-2\">\n          <div class=\"bg-gray-800 rounded-lg overflow-hidden\">\n            <!-- Source Selection -->\n            <div class=\"p-4 bg-gray-900 flex justify-between items-center\">\n              <select\n                class=\"bg-gray-800 text-white px-3 py-1 rounded-lg border border-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500\"\n                bind:value={selectedProviderId}\n                on:change={handleProviderChange}\n                aria-label=\"Select video provider\"\n              >\n                {#each providers as provider}\n                  <option value={provider.id}>{provider.name}</option>\n                {/each}\n              </select>\n\n              <div class=\"ml-2\">\n                <WatchlistButton\n                  id={media.id}\n                  type={mediaType}\n                  title={media.title || media.name || ''}\n                  posterPath={media.poster_path || null}\n                  voteAverage={media.vote_average}\n                />\n              </div>\n            </div>\n\n            <!-- Video Player -->\n            <div class=\"aspect-video\">\n              <VideoPlayer\n                bind:this={videoPlayer}\n                mediaId={media.id}\n                {mediaType}\n                title={media.title || media.name || ''}\n                season={selectedSeason}\n                episode={selectedEpisode}\n              />\n            </div>\n\n            <div class=\"p-6\">\n              <div class=\"flex items-start justify-between gap-4 mb-4\">\n                <div class=\"flex-1\">\n                  <div class=\"flex items-center gap-2 mb-1\">\n                    <span class=\"px-2 py-0.5 bg-primary-500 text-xs font-medium rounded-full\">\n                      {mediaType === 'tv' ? 'TV Series' : 'Movie'}\n                    </span>\n                    {#if mediaType === 'tv'}\n                      <span class=\"text-sm text-gray-400\">\n                        {seasonCount} {seasonCount === 1 ? 'Season' : 'Seasons'} • {episodeCount} {episodeCount === 1 ? 'Episode' : 'Episodes'}\n                      </span>\n                    {/if}\n                  </div>\n                  <div class=\"flex items-center justify-between gap-4\">\n                    <h1 class=\"text-3xl font-bold\">{media.title || media.name}</h1>\n                    <div class=\"flex items-center gap-2\">\n                      {#if mediaType === 'tv'}\n                        <button\n                          type=\"button\"\n                          class=\"px-4 py-2 bg-primary-500 text-white rounded-lg font-medium hover:bg-primary-600 transition-colors whitespace-nowrap\"\n                          on:click={() => showEpisodeModal = true}\n                        >\n                          {selectedSeason && selectedEpisode\n                            ? `S${selectedSeason}E${selectedEpisode}`\n                            : 'Select Episode'}\n                        </button>\n\n                        {#if selectedSeason && selectedEpisode}\n                          <NextEpisode\n                            mediaId={media.id}\n                            currentSeason={selectedSeason}\n                            currentEpisode={selectedEpisode}\n                            onSelect={handleNextEpisode}\n                          />\n                        {/if}\n                      {/if}\n                    </div>\n                  </div>\n                </div>\n              </div>\n\n              <div class=\"flex items-center gap-4 text-sm text-gray-400 mb-6\">\n                <span>{formatDate(media.release_date || media.first_air_date)}</span>\n                {#if media.runtime}\n                  <span>•</span>\n                  <span>{media.runtime} min</span>\n                {/if}\n                <span>•</span>\n                <div class=\"flex items-center gap-1\">\n                  <svg class=\"w-4 h-4 text-yellow-400\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n                    <path d=\"M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z\" />\n                  </svg>\n                  <span>{media.vote_average.toFixed(1)}</span>\n                </div>\n              </div>\n\n              <p class=\"text-gray-300 mb-6\">{media.overview}</p>\n\n              {#if media.genres?.length}\n                <div class=\"flex flex-wrap gap-2\">\n                  {#each media.genres as genre}\n                    <span class=\"px-3 py-1 bg-gray-700 rounded-full text-sm\">\n                      {genre.name}\n                    </span>\n                  {/each}\n                </div>\n              {/if}\n            </div>\n          </div>\n\n          <div class=\"mt-8 space-y-6\">\n            <h2 class=\"text-2xl font-bold\">Comments</h2>\n            <CommentList\n              mediaId={media.id}\n              {mediaType}\n              season={mediaType === 'tv' ? selectedSeason : undefined}\n              episode={mediaType === 'tv' ? selectedEpisode : undefined}\n            />\n          </div>\n        </div>\n\n        <!-- Left Column: Poster -->\n        <div class=\"w-full lg:w-1/4 lg:order-1\">\n          <div class=\"lg:sticky lg:top-24\">\n            <div class=\"hidden lg:block aspect-[2/3] rounded-lg overflow-hidden shadow-xl\">\n              <img\n                src={media.poster_path ? `/api/image/w500${media.poster_path}` : '/placeholder-poster.jpg'}\n                alt={media.title || media.name || ''}\n                class=\"w-full h-full object-cover\"\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n<EpisodeSelector\n  mediaId={media.id}\n  bind:showModal={showEpisodeModal}\n  on:select={handleEpisodeSelect}\n/>\n"
  },
  {
    "path": "src/routes/movies/+page.server.ts",
    "content": "import type { ServerLoad } from \"@sveltejs/kit\";\nimport { TMDBService } from \"$lib/services/tmdb\";\nimport type { FilterOptions } from \"$lib/types/filters\";\nimport { parseQueryString } from \"$lib/types/filters\";\n\nconst tmdbService = new TMDBService();\n\nexport const load: ServerLoad = async ({ url }) => {\n  try {\n\n    const filters = parseQueryString(url.search);\n\n\n    const genres = await tmdbService.getMovieGenres();\n\n\n    const {\n      results: movies,\n      total_pages: totalPages,\n      total_results: totalResults,\n    } = await fetchFilteredMovies(filters);\n\n    return {\n      movies,\n      genres,\n      totalPages: Math.min(totalPages, 500),\n      totalResults,\n    };\n  } catch (error) {\n    console.error(\"Failed to load movies:\", error);\n    return {\n      movies: [],\n      genres: [],\n      totalPages: 0,\n      totalResults: 0,\n      error: \"Failed to load movies\",\n    };\n  }\n};\n\nasync function fetchFilteredMovies(filters: FilterOptions) {\n  const {\n    query,\n    genres,\n    year,\n    rating,\n    sortBy = \"popularity\",\n    sortOrder = \"desc\",\n    page = 1,\n  } = filters;\n\n\n  if (query) {\n    return tmdbService.searchMovies(query, page);\n  }\n\n\n  const params: Record<string, string | number> = {\n    page,\n    sort_by: `${sortBy}.${sortOrder}`,\n    \"vote_count.gte\": 100,\n  };\n\n\n  if (genres && genres.length > 0) {\n    params.with_genres = genres.join(\",\");\n  }\n\n\n  if (year) {\n    params.primary_release_year = year;\n  }\n\n\n  if (rating) {\n    params[\"vote_average.gte\"] = rating;\n  }\n\n\n  params.with_original_language = \"en\";\n\n\n  return tmdbService.discoverMovies(params);\n}\n"
  },
  {
    "path": "src/routes/movies/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import MediaCard from '$lib/components/MediaCard.svelte';\n  import MediaFilters from '$lib/components/MediaFilters.svelte';\n  import VideoPlayer from '$lib/components/VideoPlayer.svelte';\n  import type { TMDBMediaResponse } from '$lib/types/tmdb';\n\n  let movies: TMDBMediaResponse[] = [];\n  let loading = true;\n  let error: string | null = null;\n  let page = 1;\n  let totalPages = 1;\n  let selectedSort = 'trending';\n  let selectedGenre = '';\n  let selectedYear = '';\n  let selectedMovie: TMDBMediaResponse | null = null;\n\n  async function fetchMovies(currentPage = 1, reset = false) {\n    loading = true;\n    error = null;\n\n    try {\n      let url = '/api/movies';\n      const params = new URLSearchParams({\n        page: currentPage.toString(),\n        sort: selectedSort,\n        ...(selectedGenre && { genre: selectedGenre }),\n        ...(selectedYear && { year: selectedYear })\n      });\n\n      const response = await fetch(`${url}?${params}`);\n      if (!response.ok) {\n        throw new Error('Failed to fetch movies');\n      }\n\n      const data = await response.json();\n\n      if (reset) {\n        movies = data.results;\n      } else {\n        movies = [...movies, ...data.results];\n      }\n\n      totalPages = Math.min(data.total_pages, 500);\n    } catch (err) {\n      console.error('Error fetching movies:', err);\n      error = 'Failed to load movies';\n    } finally {\n      loading = false;\n    }\n  }\n\n  async function handleFilter(event: CustomEvent<{ sort: string; genre: string; year: string }>) {\n    const { sort, genre, year } = event.detail;\n    selectedSort = sort;\n    selectedGenre = genre;\n    selectedYear = year;\n    page = 1;\n    await fetchMovies(1, true);\n  }\n\n  async function loadMore() {\n    if (!loading && page < totalPages) {\n      const nextPage = page + 1;\n      await fetchMovies(nextPage);\n      page = nextPage;\n    }\n  }\n\n  function handleMovieClick(movie: TMDBMediaResponse) {\n    selectedMovie = movie;\n  }\n\n  onMount(() => {\n    fetchMovies();\n  });\n</script>\n\n<div class=\"container mx-auto px-4 py-8\">\n  <div class=\"flex justify-between items-center mb-8\">\n    <h1 class=\"text-3xl font-bold\">Movies</h1>\n  </div>\n\n  <MediaFilters\n    type=\"movie\"\n    {selectedSort}\n    {selectedGenre}\n    {selectedYear}\n    on:filter={handleFilter}\n  />\n\n  {#if selectedMovie}\n    <div class=\"mb-8 bg-gray-800 rounded-lg overflow-hidden\">\n      <div class=\"p-6\">\n        <div class=\"flex justify-between items-center mb-4\">\n          <h2 class=\"text-2xl font-bold\">{selectedMovie.title}</h2>\n          <button\n            type=\"button\"\n            class=\"text-gray-400 hover:text-white\"\n            on:click={() => selectedMovie = null}\n            aria-label=\"Close video player\"\n          >\n            <svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n            </svg>\n          </button>\n        </div>\n        <VideoPlayer\n          mediaId={selectedMovie.id}\n          mediaType=\"movie\"\n          title={selectedMovie.title || 'Unknown Movie'}\n        />\n      </div>\n    </div>\n  {/if}\n\n  {#if loading && movies.length === 0}\n    <div class=\"flex justify-center py-8\">\n      <div class=\"animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500\"></div>\n    </div>\n  {:else if error}\n    <div class=\"text-red-500 text-center py-8\">\n      {error}\n    </div>\n  {:else if movies.length === 0}\n    <div class=\"text-gray-400 text-center py-8\">\n      No movies found matching your criteria\n    </div>\n  {:else}\n    <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">\n      {#each movies as movie (movie.id)}\n        <div\n          class=\"cursor-pointer\"\n          on:click={() => handleMovieClick(movie)}\n          on:keydown={(e) => e.key === 'Enter' && handleMovieClick(movie)}\n          role=\"button\"\n          tabindex=\"0\"\n        >\n          <MediaCard\n            id={movie.id}\n            type=\"movie\"\n            title={movie.title || ''}\n            posterPath={movie.poster_path}\n            voteAverage={movie.vote_average}\n          />\n        </div>\n      {/each}\n    </div>\n\n    {#if page < totalPages}\n      <div class=\"flex justify-center mt-8\">\n        <button\n          type=\"button\"\n          class=\"px-6 py-3 bg-primary-500 hover:bg-primary-600 text-white font-semibold rounded-lg transition-colors disabled:opacity-50\"\n          on:click={loadMore}\n          disabled={loading}\n        >\n          {loading ? 'Loading...' : 'Load More'}\n        </button>\n      </div>\n    {/if}\n  {/if}\n</div>\n"
  },
  {
    "path": "src/routes/register/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { authStore } from '$lib/stores/auth';\n  import { goto } from '$app/navigation';\n  import Captcha from '$lib/components/Captcha.svelte';\n\n  let username = '';\n  let email = '';\n  let password = '';\n  let confirmPassword = '';\n  let loading = false;\n  let error = '';\n  let captchaVerified = false;\n  let captchaId = '';\n  let captchaAnswer = '';\n\n  function validatePassword(pass: string): string | null {\n    if (pass.length < 8) {\n      return 'Password must be at least 8 characters long';\n    }\n    if (!/[A-Z]/.test(pass)) {\n      return 'Password must contain at least one uppercase letter';\n    }\n    if (!/[a-z]/.test(pass)) {\n      return 'Password must contain at least one lowercase letter';\n    }\n    if (!/[0-9]/.test(pass)) {\n      return 'Password must contain at least one number';\n    }\n    return null;\n  }\n\n  function validateEmail(email: string): boolean {\n    const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n    return emailRegex.test(email);\n  }\n\n  function handleCaptchaVerify(event: CustomEvent<{ valid: boolean; captchaId: string; answer: string }>) {\n    captchaVerified = event.detail.valid;\n    if (!captchaVerified) {\n      error = 'Invalid captcha, please try again';\n      captchaId = '';\n      captchaAnswer = '';\n    } else {\n      error = '';\n      captchaId = event.detail.captchaId;\n      captchaAnswer = event.detail.answer;\n    }\n  }\n\n  async function handleSubmit() {\n    error = '';\n\n\n    if (!username || !password || !confirmPassword) {\n      error = 'Please fill in all required fields';\n      return;\n    }\n\n\n    if (username.length < 3) {\n      error = 'Username must be at least 3 characters long';\n      return;\n    }\n\n\n    if (email && !validateEmail(email)) {\n      error = 'Please enter a valid email address';\n      return;\n    }\n\n\n    const passwordError = validatePassword(password);\n    if (passwordError) {\n      error = passwordError;\n      return;\n    }\n\n\n    if (password !== confirmPassword) {\n      error = 'Passwords do not match';\n      return;\n    }\n\n\n    if (!captchaVerified) {\n      error = 'Please complete the captcha verification';\n      return;\n    }\n\n    loading = true;\n\n    try {\n      const success = await authStore.register(\n        username,\n        email || null,\n        password,\n        captchaId,\n        captchaAnswer,\n      );\n      if (success) {\n        goto('/');\n      } else {\n        error = 'Registration failed. This username or email might already be taken.';\n      }\n    } catch (err) {\n      console.error('Registration error:', err);\n      error = 'An error occurred during registration. Please try again.';\n    } finally {\n      loading = false;\n    }\n  }\n</script>\n\n<div class=\"min-h-[calc(100vh-16rem)] flex items-center justify-center px-4\">\n  <div class=\"w-full max-w-md\">\n    <div class=\"bg-gray-800 rounded-lg shadow-xl p-8\">\n      <h1 class=\"text-3xl font-bold text-center mb-8\">Create Account</h1>\n\n      <form on:submit|preventDefault={handleSubmit} class=\"space-y-6\" autocomplete=\"off\">\n        {#if error}\n          <div class=\"bg-red-500/10 border border-red-500 text-red-500 px-4 py-3 rounded\">\n            {error}\n          </div>\n        {/if}\n\n        <div>\n          <label for=\"username\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n            Username <span class=\"text-red-500\">*</span>\n          </label>\n          <input\n            type=\"text\"\n            id=\"username\"\n            bind:value={username}\n            class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n            required\n            minlength=\"3\"\n            autocomplete=\"off\"\n          />\n          <p class=\"mt-1 text-sm text-gray-400\">Must be at least 3 characters long</p>\n        </div>\n\n        <div>\n          <label for=\"email\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n            Email <span class=\"text-gray-500\">(optional)</span>\n          </label>\n          <input\n            type=\"email\"\n            id=\"email\"\n            bind:value={email}\n            class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n            autocomplete=\"off\"\n          />\n        </div>\n\n        <div>\n          <label for=\"password\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n            Password <span class=\"text-red-500\">*</span>\n          </label>\n          <input\n            type=\"password\"\n            id=\"password\"\n            bind:value={password}\n            class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n            required\n            minlength=\"8\"\n            autocomplete=\"new-password\"\n          />\n          <p class=\"mt-1 text-sm text-gray-400\">\n            Must be at least 8 characters with uppercase, lowercase, and numbers\n          </p>\n        </div>\n\n        <div>\n          <label for=\"confirm-password\" class=\"block text-sm font-medium text-gray-300 mb-2\">\n            Confirm Password <span class=\"text-red-500\">*</span>\n          </label>\n          <input\n            type=\"password\"\n            id=\"confirm-password\"\n            bind:value={confirmPassword}\n            class=\"w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n            required\n            autocomplete=\"new-password\"\n          />\n        </div>\n\n        <div>\n          <Captcha on:verify={handleCaptchaVerify} />\n        </div>\n\n        <button\n          type=\"submit\"\n          class=\"w-full py-3 px-4 bg-primary-500 text-white rounded-lg font-medium hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed\"\n          disabled={loading}\n        >\n          {loading ? 'Creating account...' : 'Create Account'}\n        </button>\n\n        <div class=\"text-center text-sm text-gray-400\">\n          Already have an account?\n          <a href=\"/login\" class=\"text-primary-400 hover:text-primary-300\">\n            Login\n          </a>\n        </div>\n      </form>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/routes/reset-password/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  let identifier = '';\n  let error = '';\n  let success = '';\n  let loading = false;\n\n  async function handleSubmit(event: SubmitEvent) {\n    event.preventDefault();\n    loading = true;\n    error = '';\n    success = '';\n\n    try {\n      const response = await csrfFetch('/api/auth/reset-password/request', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({ identifier })\n      });\n\n      const data = await response.json();\n\n      if (!response.ok) {\n        throw new Error(data.error || 'Failed to request password reset');\n      }\n\n      success = data.message;\n    } catch (e) {\n      error = e instanceof Error ? e.message : 'An error occurred';\n    } finally {\n      loading = false;\n    }\n  }\n</script>\n\n<div class=\"min-h-[calc(100vh-4rem)] bg-gray-900 flex items-center justify-center px-4\">\n  <div class=\"max-w-md w-full\">\n    <div class=\"bg-gray-800 rounded-lg shadow-xl p-8\">\n      <div class=\"mb-8\">\n        <h1 class=\"text-3xl font-bold text-center text-white\">\n          Reset Password\n        </h1>\n        <p class=\"mt-2 text-center text-gray-400\">\n          Enter your username or email to receive password reset instructions\n        </p>\n      </div>\n\n      <form class=\"space-y-6\" on:submit={handleSubmit}>\n        {#if error}\n          <div class=\"bg-red-500/10 border border-red-500 text-red-500 px-4 py-3 rounded-lg\">\n            {error}\n          </div>\n        {/if}\n\n        {#if success}\n          <div class=\"bg-green-500/10 border border-green-500 text-green-500 px-4 py-3 rounded-lg\">\n            {success}\n          </div>\n        {/if}\n\n        <div class=\"space-y-2\">\n          <label for=\"identifier\" class=\"block text-sm font-medium text-gray-300\">\n            Username or Email\n          </label>\n          <input\n            id=\"identifier\"\n            name=\"identifier\"\n            type=\"text\"\n            bind:value={identifier}\n            required\n            class=\"appearance-none block w-full px-4 py-3 border border-gray-700 bg-gray-800 text-white placeholder-gray-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors\"\n            placeholder=\"Enter your username or email\"\n          />\n        </div>\n\n        <div class=\"flex items-center justify-between\">\n          <a\n            href=\"/login\"\n            class=\"text-sm text-primary-400 hover:text-primary-300 transition-colors\"\n          >\n            Back to login\n          </a>\n        </div>\n\n        <button\n          type=\"submit\"\n          disabled={loading}\n          class=\"w-full flex justify-center items-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n        >\n          {#if loading}\n            <svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n              <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n              <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n            </svg>\n            <span>Sending...</span>\n          {:else}\n            Send Reset Instructions\n          {/if}\n        </button>\n      </form>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/routes/reset-password/[token]/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { goto } from '$app/navigation';\n  import { page } from '$app/stores';\n  import { csrfFetch } from '$lib/utils/csrf';\n\n  let newPassword = '';\n  let confirmPassword = '';\n  let error = '';\n  let success = '';\n  let loading = false;\n\n  $: token = $page.params.token;\n\n  function validatePassword(password: string): string | null {\n    if (password.length < 8) {\n      return 'Password must be at least 8 characters long';\n    }\n    if (!/[A-Z]/.test(password)) {\n      return 'Password must contain at least one uppercase letter';\n    }\n    if (!/[a-z]/.test(password)) {\n      return 'Password must contain at least one lowercase letter';\n    }\n    if (!/[0-9]/.test(password)) {\n      return 'Password must contain at least one number';\n    }\n    return null;\n  }\n\n  async function handleSubmit(event: SubmitEvent) {\n    event.preventDefault();\n    loading = true;\n    error = '';\n    success = '';\n\n    const passwordError = validatePassword(newPassword);\n    if (passwordError) {\n      error = passwordError;\n      loading = false;\n      return;\n    }\n\n    if (newPassword !== confirmPassword) {\n      error = 'Passwords do not match';\n      loading = false;\n      return;\n    }\n\n    try {\n      const response = await csrfFetch('/api/auth/reset-password/reset', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({ token, newPassword })\n      });\n\n      const data = await response.json();\n\n      if (!response.ok) {\n        if (response.status === 429) {\n          throw new Error('Too many attempts. Please try again later.');\n        }\n        throw new Error(data.error || 'Failed to reset password');\n      }\n\n      success = data.message;\n      setTimeout(() => {\n        goto('/login');\n      }, 2000);\n    } catch (e) {\n      error = e instanceof Error ? e.message : 'An error occurred';\n      if (error.includes('Invalid or expired')) {\n        setTimeout(() => {\n          goto('/reset-password');\n        }, 2000);\n      }\n    } finally {\n      loading = false;\n    }\n  }\n</script>\n\n<div class=\"min-h-[calc(100vh-4rem)] bg-gray-900 flex items-center justify-center px-4\">\n  <div class=\"max-w-md w-full\">\n    <div class=\"bg-gray-800 rounded-lg shadow-xl p-8\">\n      <div class=\"mb-8\">\n        <h1 class=\"text-3xl font-bold text-center text-white\">\n          Set New Password\n        </h1>\n        <p class=\"mt-2 text-center text-gray-400\">\n          Please choose a strong password that includes uppercase and lowercase letters, numbers, and is at least 8 characters long.\n        </p>\n      </div>\n\n      <form class=\"space-y-6\" on:submit={handleSubmit}>\n        {#if error}\n          <div class=\"bg-red-500/10 border border-red-500 text-red-500 px-4 py-3 rounded-lg\">\n            {error}\n          </div>\n        {/if}\n\n        {#if success}\n          <div class=\"bg-green-500/10 border border-green-500 text-green-500 px-4 py-3 rounded-lg\">\n            {success}\n          </div>\n        {/if}\n\n        <div class=\"space-y-4\">\n          <div class=\"space-y-2\">\n            <label for=\"new-password\" class=\"block text-sm font-medium text-gray-300\">\n              New Password\n            </label>\n            <input\n              id=\"new-password\"\n              name=\"new-password\"\n              type=\"password\"\n              bind:value={newPassword}\n              required\n              minlength=\"8\"\n              class=\"appearance-none block w-full px-4 py-3 border border-gray-700 bg-gray-800 text-white placeholder-gray-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors\"\n              placeholder=\"Enter your new password\"\n            />\n          </div>\n\n          <div class=\"space-y-2\">\n            <label for=\"confirm-password\" class=\"block text-sm font-medium text-gray-300\">\n              Confirm Password\n            </label>\n            <input\n              id=\"confirm-password\"\n              name=\"confirm-password\"\n              type=\"password\"\n              bind:value={confirmPassword}\n              required\n              minlength=\"8\"\n              class=\"appearance-none block w-full px-4 py-3 border border-gray-700 bg-gray-800 text-white placeholder-gray-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors\"\n              placeholder=\"Confirm your new password\"\n            />\n          </div>\n        </div>\n\n        <button\n          type=\"submit\"\n          disabled={loading}\n          class=\"w-full flex justify-center items-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n        >\n          {#if loading}\n            <svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n              <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n              <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n            </svg>\n            <span>Resetting password...</span>\n          {:else}\n            Reset Password\n          {/if}\n        </button>\n      </form>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "src/routes/search/+page.server.ts",
    "content": "import type { PageServerLoad } from \"./$types\";\nimport { TMDBService } from \"$lib/services/tmdb\";\n\nconst tmdb = new TMDBService();\n\nexport const load = (async ({ url }) => {\n  const query = url.searchParams.get(\"query\");\n  const page = parseInt(url.searchParams.get(\"page\") || \"1\");\n\n  if (!query) {\n    return { results: [], totalPages: 0 };\n  }\n\n  const data = await tmdb.searchMulti(query, page);\n  const results = data.results.filter(\n    (item) =>\n      (item.media_type === \"movie\" || item.media_type === \"tv\") &&\n      item.poster_path &&\n      item.vote_average > 0\n  );\n\n  return {\n    results,\n    totalPages: Math.min(data.total_pages, 500),\n  };\n}) satisfies PageServerLoad;\n"
  },
  {
    "path": "src/routes/search/+page.svelte",
    "content": "<script lang=\"ts\">\n  import MediaCard from '$lib/components/MediaCard.svelte';\n  import { page } from '$app/stores';\n  import { onMount } from 'svelte';\n\n  export let data;\n  $: results = data.results;\n  $: totalPages = data.totalPages;\n  $: searchQuery = $page.url.searchParams.get('query') || '';\n\n  let mounted = false;\n  onMount(() => {\n    mounted = true;\n  });\n</script>\n\n<div class=\"container mx-auto px-4 py-8\">\n  <h1 class=\"text-3xl font-bold text-white mb-4\">Search</h1>\n\n  <form method=\"get\" data-sveltekit-reload class=\"mb-8\">\n    <div class=\"flex gap-2\">\n      <input\n        name=\"query\"\n        value={searchQuery}\n        placeholder=\"Search movies and TV shows...\"\n        class=\"flex-1 px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent\"\n      />\n      <button\n        type=\"submit\"\n        class=\"px-6 py-3 bg-primary-500 text-white rounded-lg font-medium hover:bg-primary-600 transition-colors\"\n      >\n        Search\n      </button>\n    </div>\n  </form>\n\n  {#if !searchQuery}\n    <div class=\"text-center py-12\">\n      <h2 class=\"text-xl text-gray-400\">Enter a search term</h2>\n    </div>\n  {:else if results.length === 0}\n    <div class=\"text-center py-12\">\n      <h2 class=\"text-xl text-gray-400\">No results found</h2>\n    </div>\n  {:else}\n    <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">\n      {#if mounted}\n        {#each results as item (item.id)}\n          <MediaCard\n            id={item.id}\n            type={item.media_type === 'movie' ? 'movie' : 'tv'}\n            title={item.media_type === 'movie' ? (item.title || '') : (item.name || '')}\n            posterPath={item.poster_path}\n            voteAverage={item.vote_average}\n          />\n        {/each}\n      {/if}\n    </div>\n\n    {#if totalPages > 1}\n      <div class=\"mt-8 flex justify-center gap-4\">\n        {#if parseInt($page.url.searchParams.get('page') || '1') > 1}\n          <a\n            href=\"/search?query={searchQuery}&page={parseInt($page.url.searchParams.get('page') || '1') - 1}\"\n            data-sveltekit-reload\n            class=\"px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600\"\n          >\n            Previous\n          </a>\n        {/if}\n\n        <span class=\"px-4 py-2 text-white\">\n          Page {$page.url.searchParams.get('page') || '1'} of {totalPages}\n        </span>\n\n        {#if parseInt($page.url.searchParams.get('page') || '1') < totalPages}\n          <a\n            href=\"/search?query={searchQuery}&page={parseInt($page.url.searchParams.get('page') || '1') + 1}\"\n            data-sveltekit-reload\n            class=\"px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600\"\n          >\n            Next\n          </a>\n        {/if}\n      </div>\n    {/if}\n  {/if}\n</div>\n"
  },
  {
    "path": "src/routes/tv/+page.server.ts",
    "content": "import type { ServerLoad } from \"@sveltejs/kit\";\nimport { TMDBService } from \"$lib/services/tmdb\";\nimport type { FilterOptions } from \"$lib/types/filters\";\nimport { parseQueryString } from \"$lib/types/filters\";\n\nconst tmdbService = new TMDBService();\n\nexport const load: ServerLoad = async ({ url }) => {\n  try {\n\n    const filters = parseQueryString(url.search);\n\n\n    const genres = await tmdbService.getTVGenres();\n\n\n    const {\n      results: shows,\n      total_pages: totalPages,\n      total_results: totalResults,\n    } = await fetchFilteredShows(filters);\n\n    return {\n      shows,\n      genres,\n      totalPages: Math.min(totalPages, 500),\n      totalResults,\n    };\n  } catch (error) {\n    console.error(\"Failed to load TV shows:\", error);\n    return {\n      shows: [],\n      genres: [],\n      totalPages: 0,\n      totalResults: 0,\n      error: \"Failed to load TV shows\",\n    };\n  }\n};\n\nasync function fetchFilteredShows(filters: FilterOptions) {\n  const {\n    query,\n    genres,\n    year,\n    rating,\n    sortBy = \"popularity\",\n    sortOrder = \"desc\",\n    page = 1,\n  } = filters;\n\n\n  if (query) {\n    return tmdbService.searchTVShows(query, page);\n  }\n\n\n  const params: Record<string, string | number | boolean> = {\n    page,\n    sort_by: `${sortBy}.${sortOrder}`,\n    \"vote_count.gte\": 100,\n  };\n\n\n  if (genres && genres.length > 0) {\n    params.with_genres = genres.join(\",\");\n  }\n\n\n  if (year) {\n    params.first_air_date_year = year;\n  }\n\n\n  if (rating) {\n    params[\"vote_average.gte\"] = rating;\n  }\n\n\n  params.with_original_language = \"en\";\n\n\n  params.include_null_first_air_dates = false;\n  params.screened_theatrically = true;\n  params[\"with_status\"] = \"0,2,3\";\n\n\n  return tmdbService.discoverTVShows(params);\n}\n"
  },
  {
    "path": "src/routes/tv/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import MediaCard from '$lib/components/MediaCard.svelte';\n  import MediaFilters from '$lib/components/MediaFilters.svelte';\n  import VideoPlayer from '$lib/components/VideoPlayer.svelte';\n  import type { TMDBMediaResponse } from '$lib/types/tmdb';\n\n  interface Season {\n    season_number: number;\n    name: string;\n    episode_count: number;\n  }\n\n  interface Episode {\n    episode_number: number;\n    name: string;\n    overview: string;\n    air_date: string;\n  }\n\n  let shows: TMDBMediaResponse[] = [];\n  let loading = true;\n  let error: string | null = null;\n  let page = 1;\n  let totalPages = 1;\n  let selectedSort = 'trending';\n  let selectedGenre = '';\n  let selectedYear = '';\n  let selectedShow: TMDBMediaResponse | null = null;\n  let selectedSeason: number | undefined;\n  let selectedEpisode: number | undefined;\n  let seasons: Season[] = [];\n  let episodes: Episode[] = [];\n  let showEpisodeModal = false;\n\n  async function fetchShows(currentPage = 1, reset = false) {\n    loading = true;\n    error = null;\n\n    try {\n      let url = '/api/tv';\n      const params = new URLSearchParams({\n        page: currentPage.toString(),\n        sort: selectedSort,\n        ...(selectedGenre && { genre: selectedGenre }),\n        ...(selectedYear && { year: selectedYear })\n      });\n\n      const response = await fetch(`${url}?${params}`);\n      if (!response.ok) {\n        throw new Error('Failed to fetch TV shows');\n      }\n\n      const data = await response.json();\n\n      if (reset) {\n        shows = data.results;\n      } else {\n        shows = [...shows, ...data.results];\n      }\n\n      totalPages = data.total_pages;\n    } catch (err) {\n      console.error('Error fetching TV shows:', err);\n      error = 'Failed to load TV shows';\n    } finally {\n      loading = false;\n    }\n  }\n\n  async function fetchSeasons(showId: number) {\n    try {\n      const response = await fetch(`/api/tv/${showId}/seasons`);\n      if (response.ok) {\n        const data = await response.json();\n        seasons = data.seasons.filter((s: Season) => s.season_number > 0);\n        if (seasons.length > 0) {\n          await selectSeason(seasons[0].season_number);\n        }\n      }\n    } catch (error) {\n      console.error('Error fetching seasons:', error);\n    }\n  }\n\n  async function selectSeason(seasonNumber: number) {\n    selectedSeason = seasonNumber;\n    selectedEpisode = undefined;\n    try {\n      const response = await fetch(`/api/tv/${selectedShow?.id}/season/${seasonNumber}`);\n      if (response.ok) {\n        const data = await response.json();\n        episodes = data.episodes;\n        if (episodes.length > 0) {\n          selectEpisode(episodes[0].episode_number);\n        }\n      }\n    } catch (error) {\n      console.error('Error fetching episodes:', error);\n    }\n  }\n\n  function selectEpisode(episodeNumber: number) {\n    selectedEpisode = episodeNumber;\n    showEpisodeModal = false;\n  }\n\n  async function handleFilter(event: CustomEvent<{ sort: string; genre: string; year: string }>) {\n    const { sort, genre, year } = event.detail;\n    selectedSort = sort;\n    selectedGenre = genre;\n    selectedYear = year;\n    page = 1;\n    await fetchShows(1, true);\n  }\n\n  async function loadMore() {\n    if (page < totalPages) {\n      page++;\n      await fetchShows(page);\n    }\n  }\n\n  async function handleShowClick(show: TMDBMediaResponse) {\n    selectedShow = show;\n    await fetchSeasons(show.id);\n    showEpisodeModal = true;\n  }\n\n  onMount(() => {\n    fetchShows();\n  });\n</script>\n\n<div class=\"container mx-auto px-4 py-8\">\n  <div class=\"flex justify-between items-center mb-8\">\n    <h1 class=\"text-3xl font-bold\">TV Shows</h1>\n  </div>\n\n  <MediaFilters\n    type=\"tv\"\n    {selectedSort}\n    {selectedGenre}\n    {selectedYear}\n    on:filter={handleFilter}\n  />\n\n  {#if selectedShow && selectedSeason && selectedEpisode}\n    <div class=\"mb-8 bg-gray-800 rounded-lg overflow-hidden\">\n      <div class=\"p-6\">\n        <div class=\"flex justify-between items-center mb-4\">\n          <div>\n            <h2 class=\"text-2xl font-bold\">{selectedShow.name}</h2>\n            <p class=\"text-gray-400\">Season {selectedSeason} Episode {selectedEpisode}</p>\n          </div>\n          <button\n            type=\"button\"\n            class=\"text-gray-400 hover:text-white\"\n            on:click={() => selectedShow = null}\n            aria-label=\"Close video player\"\n          >\n            <svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n            </svg>\n          </button>\n        </div>\n        <VideoPlayer\n          mediaId={selectedShow.id}\n          mediaType=\"tv\"\n          title={selectedShow.name || 'Unknown Show'}\n          season={selectedSeason}\n          episode={selectedEpisode}\n        />\n      </div>\n    </div>\n  {/if}\n\n  {#if loading && shows.length === 0}\n    <div class=\"flex justify-center py-8\">\n      <div class=\"animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500\"></div>\n    </div>\n  {:else if error}\n    <div class=\"text-red-500 text-center py-8\">\n      {error}\n    </div>\n  {:else if shows.length === 0}\n    <div class=\"text-gray-400 text-center py-8\">\n      No TV shows found matching your criteria\n    </div>\n  {:else}\n    <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">\n      {#each shows as show (show.id)}\n        <div\n          class=\"cursor-pointer\"\n          on:click={() => handleShowClick(show)}\n          on:keydown={(e) => e.key === 'Enter' && handleShowClick(show)}\n          role=\"button\"\n          tabindex=\"0\"\n        >\n          <MediaCard\n            id={show.id}\n            type=\"tv\"\n            title={show.name || ''}\n            posterPath={show.poster_path}\n            voteAverage={show.vote_average}\n          />\n        </div>\n      {/each}\n    </div>\n\n    {#if page < totalPages}\n      <div class=\"flex justify-center mt-8\">\n        <button\n          type=\"button\"\n          class=\"px-6 py-3 bg-primary-500 hover:bg-primary-600 text-white font-semibold rounded-lg transition-colors disabled:opacity-50\"\n          on:click={loadMore}\n          disabled={loading}\n        >\n          {loading ? 'Loading...' : 'Load More'}\n        </button>\n      </div>\n    {/if}\n  {/if}\n</div>\n\n{#if showEpisodeModal && selectedShow}\n  <div class=\"fixed inset-0 bg-black/90 flex items-center justify-center z-50\">\n    <div class=\"bg-gray-800 rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto\">\n      <div class=\"flex justify-between items-center mb-6\">\n        <h2 class=\"text-2xl font-bold\">Select Episode</h2>\n        <button\n          type=\"button\"\n          class=\"text-gray-400 hover:text-white\"\n          on:click={() => showEpisodeModal = false}\n          aria-label=\"Close episode selection\"\n        >\n          <svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n          </svg>\n        </button>\n      </div>\n\n      <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n        <!-- Seasons -->\n        <div class=\"space-y-4\">\n          <h3 class=\"text-lg font-semibold mb-2\">Seasons</h3>\n          {#each seasons as season}\n            <button\n              type=\"button\"\n              class=\"w-full p-4 rounded-lg text-left transition-colors\"\n              class:bg-primary-500={selectedSeason === season.season_number}\n              class:bg-gray-700={selectedSeason !== season.season_number}\n              on:click={() => selectSeason(season.season_number)}\n            >\n              <div class=\"font-medium\">Season {season.season_number}</div>\n              <div class=\"text-sm text-gray-400\">{season.episode_count} Episodes</div>\n            </button>\n          {/each}\n        </div>\n\n        <!-- Episodes -->\n        {#if selectedSeason && episodes.length > 0}\n          <div class=\"space-y-4\">\n            <h3 class=\"text-lg font-semibold mb-2\">Episodes</h3>\n            {#each episodes as episode}\n              <button\n                type=\"button\"\n                class=\"w-full p-4 rounded-lg text-left transition-colors\"\n                class:bg-primary-500={selectedEpisode === episode.episode_number}\n                class:bg-gray-700={selectedEpisode !== episode.episode_number}\n                on:click={() => selectEpisode(episode.episode_number)}\n              >\n                <div class=\"font-medium\">\n                  Episode {episode.episode_number}: {episode.name}\n                </div>\n                <div class=\"text-sm text-gray-400\">\n                  Air Date: {new Date(episode.air_date).toLocaleDateString()}\n                </div>\n              </button>\n            {/each}\n          </div>\n        {/if}\n      </div>\n    </div>\n  </div>\n{/if}\n"
  },
  {
    "path": "src/routes/watchlist/+page.server.ts",
    "content": "import { error } from \"@sveltejs/kit\";\nimport type { ServerLoad } from \"@sveltejs/kit\";\nimport { watchlistService } from \"$lib/server/services/watchlist\";\nimport { TMDBService } from \"$lib/services/tmdb\";\n\nexport const load: ServerLoad = async ({ locals }) => {\n  if (!locals.user) {\n    throw error(401, \"Unauthorized\");\n  }\n\n  const tmdbService = new TMDBService();\n\n  try {\n    const watchlist = await watchlistService.getWatchlist(locals.user.id);\n\n\n    const mediaDetails = await Promise.all(\n      watchlist.map(async (item) => {\n        try {\n          if (item.mediaType === \"movie\") {\n            const movie = await tmdbService.getMovieDetails(item.mediaId);\n            return {\n              ...movie,\n              mediaType: \"movie\" as const,\n              addedAt: item.addedAt,\n            };\n          } else {\n            const show = await tmdbService.getTVShowDetails(item.mediaId);\n            return {\n              ...show,\n              mediaType: \"tv\" as const,\n              addedAt: item.addedAt,\n            };\n          }\n        } catch (error) {\n          console.error(\n            `Failed to fetch details for ${item.mediaType} ${item.mediaId}:`,\n            error,\n          );\n          return null;\n        }\n      }),\n    );\n\n\n    const validMediaDetails = mediaDetails\n      .filter((item): item is NonNullable<typeof item> => item !== null)\n      .sort(\n        (a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime(),\n      );\n\n    return {\n      watchlistItems: validMediaDetails,\n    };\n  } catch (e) {\n    console.error(\"Failed to load watchlist:\", e);\n    throw error(500, \"Failed to load watchlist\");\n  }\n};\n"
  },
  {
    "path": "src/routes/watchlist/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte';\n  import { watchlistStore } from '$lib/stores/watchlist';\n  import MediaCard from '$lib/components/MediaCard.svelte';\n\n  $: items = $watchlistStore.items;\n  $: loading = $watchlistStore.loading;\n  $: error = $watchlistStore.error;\n\n  onMount(async () => {\n    try {\n      await watchlistStore.getWatchlist();\n    } catch (error) {\n      console.error('Failed to load watchlist:', error);\n    }\n  });\n</script>\n\n<div class=\"container mx-auto px-4 py-8\">\n  <h1 class=\"text-3xl font-bold mb-8\">My Watchlist</h1>\n\n  {#if loading}\n    <div class=\"flex justify-center py-8\">\n      <div class=\"animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500\"></div>\n    </div>\n  {:else if error}\n    <div class=\"bg-red-900/50 text-red-200 p-4 rounded-lg text-center\">\n      {error}\n    </div>\n  {:else if items.length === 0}\n    <div class=\"bg-gray-800/50 text-gray-400 p-8 rounded-lg text-center\">\n      <p class=\"text-lg mb-2\">Your watchlist is empty</p>\n      <p class=\"text-sm text-gray-500\">Add movies or TV shows to watch later!</p>\n    </div>\n  {:else}\n    <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">\n      {#each items as item (item.id)}\n        <MediaCard\n          id={item.mediaId}\n          type={item.mediaType === 'movie' ? 'movie' : 'tv'}\n          title={item.title}\n          posterPath={item.posterPath}\n          voteAverage={item.voteAverage}\n        />\n      {/each}\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "svelte.config.js",
    "content": "import adapter from \"@sveltejs/adapter-node\";\nimport { preprocessMeltUI } from \"@melt-ui/pp\";\nimport sequence from \"svelte-sequential-preprocessor\";\nimport preprocess from \"svelte-preprocess\";\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n  kit: {\n    adapter: adapter({\n      out: 'build',\n      precompress: true,\n      envPrefix: 'APP_',\n      polyfill: true,\n      external: [],\n    }),\n    csrf: {\n      checkOrigin: true,\n    },\n  },\n  preprocess: sequence([\n    preprocess({\n      postcss: true,\n    }),\n    preprocessMeltUI(),\n  ]),\n};\n\nexport default config;\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\"./src/**/*.{html,js,svelte,ts}\"],\n  theme: {\n    extend: {\n      colors: {\n        primary: {\n          50: \"#f0f9ff\",\n          100: \"#e0f2fe\",\n          200: \"#bae6fd\",\n          300: \"#7dd3fc\",\n          400: \"#38bdf8\",\n          500: \"#0ea5e9\",\n          600: \"#0284c7\",\n          700: \"#0369a1\",\n          800: \"#075985\",\n          900: \"#0c4a6e\",\n          950: \"#082f49\",\n        },\n      },\n      animation: {\n        \"spin-slow\": \"spin 3s linear infinite\",\n      },\n      transitionProperty: {\n        \"max-height\": \"max-height\",\n      },\n    },\n  },\n  plugins: [\n    require(\"@tailwindcss/typography\"),\n    require(\"@tailwindcss/forms\"),\n    require(\"@tailwindcss/aspect-ratio\"),\n  ],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./.svelte-kit/tsconfig.json\",\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"bundler\"\n  },\n  \"include\": [\n    \".svelte-kit/ambient.d.ts\",\n    \".svelte-kit/types/**/$types.d.ts\",\n    \"vite.config.ts\",\n    \"src/**/*.js\",\n    \"src/**/*.ts\",\n    \"src/**/*.svelte\",\n    \"tests/**/*.js\",\n    \"tests/**/*.ts\",\n    \"tests/**/*.svelte\"\n  ]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { sveltekit } from \"@sveltejs/kit/vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [sveltekit()],\n  test: {\n    environment: \"node\",\n    include: [\"src/**/*.test.ts\"],\n  },\n});\n"
  }
]