{"id":50516411,"url":"https://github.com/lorem-ipsumm/media-center","last_synced_at":"2026-06-03T00:30:37.683Z","repository":{"id":360547786,"uuid":"1250656444","full_name":"lorem-ipsumm/media-center","owner":"lorem-ipsumm","description":"Local media player to watch media files stored on your local network","archived":false,"fork":false,"pushed_at":"2026-05-26T21:12:11.000Z","size":368,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T23:11:39.725Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lorem-ipsumm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-26T21:05:29.000Z","updated_at":"2026-05-26T21:12:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lorem-ipsumm/media-center","commit_stats":null,"previous_names":["lorem-ipsumm/media-center"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/lorem-ipsumm/media-center","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lorem-ipsumm%2Fmedia-center","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lorem-ipsumm%2Fmedia-center/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lorem-ipsumm%2Fmedia-center/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lorem-ipsumm%2Fmedia-center/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lorem-ipsumm","download_url":"https://codeload.github.com/lorem-ipsumm/media-center/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lorem-ipsumm%2Fmedia-center/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33843611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-03T00:30:36.180Z","updated_at":"2026-06-03T00:30:37.672Z","avatar_url":"https://github.com/lorem-ipsumm.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Media Center\n\n![Screenshot](screenshots/1.jpg)\n\nA local media browser and playback controller built on a React + Hono full-stack Vite setup. The app scans a configured directory on the host machine, lists video files grouped by folder, and controls a running `mpv` process via its IPC socket — all from a browser UI that works on both desktop and mobile.\n\n---\n\n## Stack\n\n| Layer | Technology |\n|---|---|\n| Frontend | React 18, TypeScript, Tailwind CSS v4 |\n| Components | shadcn/ui (Radix UI primitives) |\n| State / data fetching | TanStack Query v5, Zustand |\n| API server | Hono (mounted inside Vite dev server) |\n| Runtime / package manager | Bun |\n| Media player | mpv (controlled via Unix IPC socket) |\n\n---\n\n## Environment\n\nA `.env` file is required in the project root. It is loaded by `vite.config.ts` using `loadEnv` and merged into `process.env` so the Hono API can read it.\n\n```\nMEDIA_PATH=/absolute/path/to/your/media/root\n```\n\nThe `.env` file is gitignored. See `.env.example` for the required variables.\n\n---\n\n## Dev Commands\n\n```bash\nbun run dev       # Start dev server (frontend + API on the same origin)\nbun run build     # Production build\nbun run lint      # ESLint\nbun run preview   # Preview the production build\n```\n\nThe dev server runs on `http://localhost:5173`. API routes are available at `http://localhost:5173/api/*`.\n\n---\n\n## Project Structure\n\n```\nmedia-center/\n├── api/                        # Hono API server (Node.js, runs inside Vite)\n│   ├── app.ts                  # All API routes and mpv IPC logic\n│   ├── dev.ts                  # Vite dev-server entry point (just re-exports app)\n│   └── tsconfig.json           # TypeScript config scoped to the API\n│\n├── src/                        # React frontend\n│   ├── main.tsx                # App entry — mounts QueryClientProvider + ThemeProvider\n│   ├── App.tsx                 # Root component — media browser UI, search, play dialog\n│   ├── App.css                 # Minimal root height rule\n│   ├── index.css               # Tailwind import, CSS variable theme tokens (light + dark)\n│   │\n│   ├── components/\n│   │   ├── providers/\n│   │   │   └── theme-provider.tsx      # Applies \"light\"/\"dark\" class to \u003chtml\u003e, reads useThemeStore\n│   │   └── ui/\n│   │       ├── player-bar.tsx          # Persistent bottom player bar (see below)\n│   │       ├── button.tsx              # shadcn Button\n│   │       ├── dialog.tsx              # shadcn Dialog (used for play confirmation)\n│   │       ├── dropdown-menu.tsx       # shadcn DropdownMenu (used for subtitle selector)\n│   │       └── context-menu.tsx        # shadcn ContextMenu\n│   │\n│   └── lib/\n│       ├── utils.ts                    # cn() helper (clsx + tailwind-merge)\n│       └── hooks/\n│           ├── api/\n│           │   ├── use-directory-content.ts  # useMediaFiles, useDirectoryContent hooks + types\n│           │   └── use-player.ts             # All player hooks + types (see below)\n│           └── store/\n│               └── use-theme-store.ts        # Zustand store — persists \"light\" | \"dark\" theme\n│\n├── packages/                   # Shared code between frontend and API (aliased as @shared)\n├── public/                     # Static assets\n├── vite.config.ts              # Vite config — loads .env into process.env, mounts Hono\n├── index.html                  # HTML entry point\n├── .env                        # Local env vars (gitignored)\n├── .env.example                # Template showing required variables\n└── package.json\n```\n\n---\n\n## API Routes\n\nAll routes are prefixed with `/api` and defined in `api/app.ts`.\n\n### Media browsing\n\n| Method | Route | Description |\n|---|---|---|\n| `GET` | `/api/browse?path=\u003cdir\u003e` | Lists all entries in a directory with type and size |\n| `GET` | `/api/media?path=\u003cdir\u003e` | Lists video files grouped by subdirectory. Defaults to `MEDIA_PATH`. Filters by `VIDEO_EXTENSIONS` |\n\n### Player control\n\nAll player routes communicate with mpv via a Unix IPC socket at `/tmp/mpv-media-center.sock`. The status route checks for the socket file directly, so it reconnects to a running mpv even after the API server restarts.\n\n| Method | Route | Body | Description |\n|---|---|---|---|\n| `GET` | `/api/player/status` | — | Returns full player state. `{ playing, paused, title, position, duration, volume, fullscreen, subtitles, subtitleOffset }` |\n| `POST` | `/api/player/play` | `{ path: string }` | Kills any running mpv, spawns a new instance with the IPC socket flag |\n| `POST` | `/api/player/pause` | — | Sets `pause = true` via IPC |\n| `POST` | `/api/player/resume` | — | Sets `pause = false` via IPC |\n| `POST` | `/api/player/stop` | — | Kills mpv process or sends `quit` via IPC if process is unowned |\n| `POST` | `/api/player/seek` | `{ position: number }` | Seeks to absolute position in seconds |\n| `POST` | `/api/player/skip` | `{ seconds: number }` | Seeks relative to current position (use negative to go back) |\n| `POST` | `/api/player/volume` | `{ volume: number }` | Sets volume (0–130, matching mpv's range) |\n| `POST` | `/api/player/fullscreen` | `{ fullscreen: boolean }` | Sets fullscreen property on mpv window |\n| `POST` | `/api/player/subtitle` | `{ id: number \\| \"no\" }` | Sets active subtitle track by ID, or `\"no\"` to disable |\n| `POST` | `/api/player/subtitle-offset` | `{ offset: number }` | Sets the subtitle delay in seconds (`sub-delay` in mpv). Negative values make subtitles appear earlier, positive values later |\n\n---\n\n## Frontend Hooks\n\n### `src/lib/hooks/api/use-directory-content.ts`\n\n| Export | Description |\n|---|---|\n| `useMediaFiles(path)` | Fetches `/api/media`. Returns `MediaContent` — groups of folders with their video files |\n| `useDirectoryContent(path)` | Fetches `/api/browse`. Returns raw directory entries with type and size |\n| `MediaFile` | `{ name, path, size }` |\n| `MediaGroup` | `{ name, path, files: MediaFile[] }` |\n| `MediaContent` | `{ path, groups: MediaGroup[] }` |\n\n### `src/lib/hooks/api/use-player.ts`\n\n| Export | Description |\n|---|---|\n| `usePlayerStatus()` | Polls `/api/player/status` — every 1s while playing, every 3s while idle |\n| `usePlayFile()` | Mutation — POSTs to `/api/player/play` with a file path |\n| `usePausePlayer()` | Mutation — optimistically sets `paused: true` |\n| `useResumePlayer()` | Mutation — optimistically sets `paused: false` |\n| `useStopPlayer()` | Mutation — optimistically sets `playing: false` |\n| `useSeekPlayer()` | Mutation — optimistically updates `position` (absolute seek) |\n| `useSkipPlayer()` | Mutation — optimistically nudges `position` by ±N seconds (relative seek) |\n| `useSetVolume()` | Mutation — optimistically updates `volume` |\n| `useSetFullscreen()` | Mutation — optimistically updates `fullscreen` |\n| `useSetSubtitle()` | Mutation — optimistically updates `selected` on the subtitles array |\n| `useAdjustSubtitleOffset()` | Mutation — optimistically updates `subtitleOffset`; POSTs absolute offset in seconds to `/api/player/subtitle-offset` |\n| `PlayerStatus` | Full status shape returned by the status endpoint |\n| `SubtitleTrack` | `{ id, title, lang, selected }` |\n\n### `src/lib/hooks/store/use-theme-store.ts`\n\nZustand store persisted to `localStorage` under the key `theme-storage`.\n\n| Field / method | Description |\n|---|---|\n| `theme` | `\"light\" \\| \"dark\"` |\n| `setTheme(theme)` | Sets theme explicitly |\n| `toggleTheme()` | Flips between light and dark |\n\n---\n\n## Key Components\n\n### `src/App.tsx`\n\nThe root component. Responsibilities:\n- Renders the header (logo, folder count, theme toggle)\n- Search input that filters groups and files client-side\n- Renders `DirectoryGroup` cards, each collapsible, containing `MediaFileRow` items\n- Clicking a file opens `PlayDialog` (confirmation before sending play request)\n- Renders `PlayerBar` at the bottom when `playerStatus.playing === true`\n\n### `src/components/ui/player-bar.tsx`\n\nThe persistent playback control bar. Rendered in three rows:\n1. **Playback controls** — skip back 10s, play/pause, skip forward 10s, stop (centred)\n2. **Seek scrubber** — draggable/tappable progress bar (pointer capture for mobile drag)\n3. **Info + secondary controls** — file icon, title, timestamp / volume slider+mute / subtitle dropdown / fullscreen toggle\n\nThe **More Controls** dialog (opened via the sliders icon in the playback row) contains additional controls:\n- **Subtitle Offset** — `−` / `+` buttons adjust the subtitle delay by 0.1 s per tap; a **Reset** button returns it to 0. The current offset is displayed and highlighted in primary colour when non-zero.\n- **Display** — sets the connected display to 1920×1080 via `xrandr`.\n\n### `src/components/providers/theme-provider.tsx`\n\nReads `useThemeStore` and applies the `\"light\"` or `\"dark\"` class to `document.documentElement`. Also checks `prefers-color-scheme` on first load if no saved preference exists.\n\n---\n\n## Styling Conventions\n\n- **All colours use CSS variable-based Tailwind classes** — `bg-background`, `text-foreground`, `bg-muted`, `text-muted-foreground`, `bg-primary`, `text-primary-foreground`, `bg-card`, `border-border`, `bg-accent`, `text-destructive`, etc.\n- **Never use hardcoded colour classes** like `bg-zinc-900` or `text-gray-500`. This ensures light/dark theme switching works correctly throughout.\n- CSS variables for both themes are defined in `src/index.css`.\n\n---\n\n## Path Aliases\n\n| Alias | Resolves to |\n|---|---|\n| `@` | `./src` |\n| `@shared` | `./packages` |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Florem-ipsumm%2Fmedia-center","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Florem-ipsumm%2Fmedia-center","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Florem-ipsumm%2Fmedia-center/lists"}