{"id":47698790,"url":"https://github.com/neybar/pi_slide_show","last_synced_at":"2026-04-02T17:00:06.993Z","repository":{"id":21478703,"uuid":"24797356","full_name":"neybar/pi_slide_show","owner":"neybar","description":"Browser based slide show optimized for a Raspberry Pi","archived":false,"fork":false,"pushed_at":"2026-03-14T03:53:37.000Z","size":478,"stargazers_count":0,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-14T13:10:19.257Z","etag":null,"topics":["perl"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/neybar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2014-10-04T18:14:55.000Z","updated_at":"2026-03-14T01:06:22.000Z","dependencies_parsed_at":"2022-08-05T19:00:27.437Z","dependency_job_id":null,"html_url":"https://github.com/neybar/pi_slide_show","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/neybar/pi_slide_show","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neybar%2Fpi_slide_show","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neybar%2Fpi_slide_show/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neybar%2Fpi_slide_show/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neybar%2Fpi_slide_show/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/neybar","download_url":"https://codeload.github.com/neybar/pi_slide_show/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neybar%2Fpi_slide_show/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31310997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["perl"],"created_at":"2026-04-02T17:00:05.886Z","updated_at":"2026-04-02T17:00:06.953Z","avatar_url":"https://github.com/neybar.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pi_slide_show\n\nA Raspberry Pi photo slideshow application that displays random photos from your library in a beautiful grid layout.\n\n## Features\n\n- **Dynamic photo selection** - Randomly selects photos from your library on each refresh\n- **Individual photo swap** - Photos swap one at a time every 10 seconds with weighted random selection (older photos more likely to be replaced)\n- **Slide animations with bounce** - Heavy ball bounce effect with 3 decreasing bounces (10%, 4%, 1.5% amplitude)\n- **Panoramic photo support** - Wide photos (\u003e2:1 ratio) span multiple columns with smooth panning animation\n- **Responsive grid layout** - Two-row shelf display using Pure CSS with full layout coverage (object-fit: cover)\n- **Progressive image loading** - Fast initial display with M thumbnails, XL upgrades in background\n- **Seamless album transitions** - Pre-fetches next album in background, fades between albums with no black screen\n- **Image preloading** - Smooth transitions with no dark screens\n- **Automatic recovery system** - Watchdog monitors for stuck/invisible cells and failed image loads, automatically recovers with photo swaps\n- **EXIF orientation support** - Photos display correctly regardless of camera orientation\n- **Synology NAS support** - Uses thumbnail paths for optimized loading\n- **Cache-busting assets** - CSS/JS versioned on server restart for remote displays\n- **Docker ready** - Easy deployment with Docker Compose\n\n## Quick Start\n\n### Using Docker (Recommended)\n\n```bash\n# Clone the repository\ngit clone https://github.com/neybar/pi_slide_show.git\ncd pi_slide_show\n\n# Edit docker-compose.yml to set your photo library path\n# Then start the container\ndocker compose up -d\n\n# Visit http://localhost:3000\n```\n\nNote: The default `docker-compose.yml` uses NFS to mount photos from a Synology NAS. Edit the `volumes` section to match your network storage or use a local bind mount instead.\n\n### Using Node.js\n\n```bash\n# Clone and install\ngit clone https://github.com/neybar/pi_slide_show.git\ncd pi_slide_show\nnpm install\n\n# Configure (optional - defaults to /mnt/photo)\nexport PHOTO_LIBRARY=/path/to/your/photos\n\n# Start the server\nnpm start\n\n# Visit http://localhost:3000\n```\n\n## Configuration\n\nConfiguration can be set via `generate_slideshow.yml` or environment variables:\n\n| Setting | Environment Variable | Default | Description |\n|---------|---------------------|---------|-------------|\n| `photo_library` | `PHOTO_LIBRARY` | `/mnt/photo` | Path to photo directory |\n| `default_count` | `DEFAULT_COUNT` | `25` | Photos per page load |\n| `web_photo_dir` | `WEB_PHOTO_DIR` | `photos` | URL prefix for photos |\n| - | `PORT` | `3000` | Server port |\n| - | `LOG_LEVEL` | `info` (Docker: `error`) | Logging verbosity (error/warn/info/debug) |\n| - | `RATE_LIMIT_MAX_REQUESTS` | `100` | Max requests per minute per IP (localhost gets 50x) |\n\n### Excluding Folders\n\nTo exclude a folder from the slideshow, create an empty `.noslideshow` file in that folder:\n\n```bash\ntouch /path/to/photos/private-folder/.noslideshow\n```\n\nThe folder and all its subfolders will be skipped during photo discovery.\n\nThe following folders are always excluded:\n- Hidden folders (starting with `.`)\n- `iPhoto Library`\n- `@eaDir` (Synology thumbnail directories)\n- `#recycle` (Synology recycle bin)\n\n### Frontend Settings\n\nAnimation timing and layout behavior can be adjusted in `www/js/config.mjs`:\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `SWAP_INTERVAL` | `10000` | Time between photo swaps (ms) |\n| `PANORAMA_ASPECT_THRESHOLD` | `2.0` | Aspect ratio threshold for panorama detection |\n| `ORIENTATION_MATCH_PROBABILITY` | `0.7` | Probability to match photo orientation to container |\n| `STACKED_LANDSCAPES_PROBABILITY` | `0.3` | Probability for stacked landscapes in 1-col slots |\n| `SHRINK_ANIMATION_DURATION` | `400` | Phase A: Shrink-to-corner duration (ms) |\n| `SLIDE_IN_ANIMATION_DURATION` | `800` | Phase B \u0026 C: Gravity fill and slide-in duration (ms) |\n| `PHASE_OVERLAP_DELAY` | `200` | Delay before Phase C starts while Phase B animates (ms) |\n| `FILL_STAGGER_DELAY` | `100` | Stagger delay between fill photo slide-in animations (ms) |\n| `ENABLE_SHRINK_ANIMATION` | `true` | Set to `false` for low-powered devices |\n| `PROGRESSIVE_LOADING_ENABLED` | `true` | Enable two-stage progressive loading |\n| `INITIAL_BATCH_SIZE` | `15` | Photos to load in first batch (fast display) |\n| `INITIAL_QUALITY` | `'M'` | Initial thumbnail quality (M = medium) |\n| `FINAL_QUALITY` | `'XL'` | Final thumbnail quality after upgrade |\n| `UPGRADE_BATCH_SIZE` | `5` | Photos per upgrade batch (prevents CPU spikes) |\n| `UPGRADE_DELAY_MS` | `100` | Delay between upgrade batches (ms) |\n| `LOAD_BATCH_SIZE` | `5` | Photos per batch during initial load |\n| `DEBUG_PROGRESSIVE_LOADING` | `false` | Enable console logging for progressive loading |\n| `IMAGE_PRELOAD_TIMEOUT` | `30000` | Timeout for image preloading (ms) |\n| `PREFETCH_LEAD_TIME` | `60000` | Start pre-fetching next album 1 minute before transition (ms) |\n| `ALBUM_TRANSITION_ENABLED` | `true` | Enable seamless transitions (set false to fallback to location.reload) |\n| `ALBUM_TRANSITION_FADE_DURATION` | `1000` | Fade out/in duration for album transitions (ms) |\n| `PREFETCH_MEMORY_THRESHOLD_MB` | `100` | Skip prefetch if available memory below threshold (MB) |\n| `FORCE_RELOAD_INTERVAL` | `8` | Force full page reload every N transitions (memory hygiene) |\n| `MIN_PHOTOS_FOR_TRANSITION` | `15` | Minimum photos required for seamless transition |\n\nAdditional constants for panorama behavior, layout probabilities, and timeouts are available in `config.mjs`. See the file comments for details.\n\n## API Endpoints\n\n| Endpoint | Description |\n|----------|-------------|\n| `GET /` | Serve the slideshow viewer |\n| `GET /album/:count` | Get JSON with random photos |\n| `GET /album/fixture/:year` | Get JSON with fixed photos for testing (disabled in production) |\n| `GET /photos/*` | Serve photo files |\n\n## Development\n\n```bash\n# Install dependencies\nnpm install\n\n# Run with auto-reload\nnpm run dev\n\n# Run tests\nnpm test              # Unit and performance tests\nnpm run test:e2e      # E2E tests (requires: npx playwright install chromium)\nnpm run test:smoke    # Quick deployment health checks (\u003c 10 seconds)\nnpm run test:all      # Unit, perf, and E2E tests\nnpm run test:coverage # Unit tests with coverage report\n\n# Run long-running stability tests (optional, ~7 minutes)\nLONG_RUNNING_TEST=1 npm run test:e2e -- --grep \"Column Stability\"\n\n# Run Docker performance tests (local only, requires Docker running)\ndocker compose up -d                    # Start container\nnpm run test:perf:docker                # Run perf tests\ncat perf-results/perf-history.json      # View results (tracks history over time)\n```\n\n### Performance Tests\n\nThere are two types of performance tests, each designed for a specific purpose:\n\n**1. Album Lookup Performance** (`test/perf/album-lookup.perf.mjs`)\n\nTests the `/album/25` API endpoint performance (filesystem crawling and random selection):\n- Measures response time across multiple iterations\n- Calculates min, max, average, and p95 statistics\n- Uses random photos by design (tests real-world usage)\n- Results tracked in `perf-results/album-lookup-history.json`\n\n```bash\nnpm run test:perf:docker -- --grep \"Album Lookup\"\n```\n\n**2. Photo Loading Performance** (`test/perf/loading-by-year.perf.mjs`, `test/perf/compare-prod.perf.mjs`)\n\nTests progressive loading performance using fixed, reproducible datasets:\n- Uses JSON fixture files with pre-selected photos from different eras (2010, 2015, 2020, 2025)\n- Enables valid apples-to-apples comparisons between environments\n- Measures time-to-first-photo, thumbnail loading, and XL upgrade phases\n- Results tracked in `perf-results/loading-by-year-history.json`\n\n```bash\n# Test all fixture years\nnpm run test:perf:docker -- --grep \"Loading by Year\"\n\n# Compare local vs production (uses 2020 fixture)\nPROD_URL=http://192.168.0.6:8531 npm run test:perf:docker -- --grep \"Comparison\"\n```\n\n**Why Fixed Datasets?**\n\nRandom photos cause inconsistent results:\n- Photos from 2010 (~2MB) vs 2025 (~25MB) have vastly different load times\n- Comparing runs with different photos is invalid\n- Fixed fixtures ensure reproducible benchmarks\n\n**Creating New Fixtures**\n\nFixture files are in `test/fixtures/albums/album-YYYY.json`:\n\n```json\n{\n  \"count\": 25,\n  \"images\": [\n    { \"file\": \"photos/2020/January/IMG_001.JPG\", \"Orientation\": 1 },\n    ...\n  ],\n  \"_metadata\": {\n    \"note\": \"25 photos from 2018-2022 era\",\n    \"expectedSizes\": { \"M\": \"~50KB\", \"XL\": \"~450KB\" }\n  }\n}\n```\n\nTo add a fixture:\n1. Create `album-YYYY.json` with 25 photos from your library\n2. Include a mix of orientations (1, 3, 6, 8)\n3. Add the year to `validYears` array in `lib/routes.mjs`\n\n## Docker\n\n```bash\n# Build the image\ndocker build -t pi_slide_show .\n\n# Run with photo library mounted\ndocker run -p 3000:3000 -v /path/to/photos:/photos:ro pi_slide_show\n\n# Or use docker-compose\ndocker compose up -d\n```\n\n## Security Features\n\n- **Rate limiting** - 100 requests per minute per IP (5000/min for localhost; configurable via `RATE_LIMIT_MAX_REQUESTS`)\n- **URL length limit** - Maximum 2048 characters (returns 414 URI Too Long)\n- **Path traversal protection** - Symlink validation prevents directory escape attacks\n- **Security headers** - X-Content-Type-Options, X-Frame-Options, Content-Security-Policy\n- **Server timeouts** - Prevents slow-loris attacks\n- **YAML safe schema** - Prevents deserialization attacks\n\n## Requirements\n\n- Node.js 22+\n- Photo library with JPEG images\n\n## Documentation\n\n- [ARCHITECTURE.md](ARCHITECTURE.md) - Design decisions and project intent\n- [docs/visual-algorithm.md](docs/visual-algorithm.md) - Visual layout and animation system\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneybar%2Fpi_slide_show","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fneybar%2Fpi_slide_show","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneybar%2Fpi_slide_show/lists"}