Compare commits
3 Commits
renovate/m
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83843bb6c8 | ||
|
|
c2fe0fdc95 | ||
|
|
880fbc902d |
149
CHANNELS_DVR_INTEGRATION.md
Normal file
149
CHANNELS_DVR_INTEGRATION.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Channels DVR Integration for Seerr
|
||||
|
||||
**Status:** Phase 1 Complete (Core Integration)
|
||||
**Date:** 2026-02-20
|
||||
**Implemented by:** Synapse (Opus → Sonnet)
|
||||
|
||||
## Overview
|
||||
|
||||
Added Channels DVR as a 4th media server backend to Seerr (alongside Jellyfin, Plex, Emby).
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Media Server Type Enum (`server/constants/server.ts`)
|
||||
- Added `CHANNELS_DVR = 4` to `MediaServerType` enum
|
||||
|
||||
### 2. API Client (`server/api/channelsdvr.ts`)
|
||||
- Full REST API client for Channels DVR
|
||||
- Methods:
|
||||
- `getShows()` - List all TV shows
|
||||
- `getShow(id)` - Get specific show
|
||||
- `getShowEpisodes(id)` - Get episodes for a show
|
||||
- `getMovies()` - List all movies
|
||||
- `getMovie(id)` - Get specific movie
|
||||
- `testConnection()` - Connectivity test
|
||||
- TypeScript interfaces for all API responses
|
||||
|
||||
### 3. Library Scanner (`server/lib/scanners/channelsdvr/index.ts`)
|
||||
- Scans Channels DVR library and maps to Seerr
|
||||
- **Key feature:** TMDb ID lookup by title/year search
|
||||
- Processes movies and TV shows
|
||||
- Handles episode/season grouping
|
||||
- Tracks processing status
|
||||
|
||||
### 4. Settings Integration (`server/lib/settings/index.ts`)
|
||||
- Added `ChannelsDVRSettings` interface
|
||||
- Added to `AllSettings` with default initialization
|
||||
- Configuration fields:
|
||||
- `name`: Display name
|
||||
- `url`: Channels DVR server URL (e.g., http://192.168.0.15:8089)
|
||||
- `libraries`: Library configuration array
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **User configures Channels DVR URL** in Seerr settings
|
||||
2. **Scanner connects** via REST API (no auth needed!)
|
||||
3. **Fetches all content** (movies + TV shows)
|
||||
4. **Maps to TMDb** by searching title + year
|
||||
5. **Processes into Seerr database** for request management
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Why TMDb Search Instead of Direct IDs?
|
||||
- Channels DVR doesn't provide TMDb/IMDb IDs in API
|
||||
- Uses program_id (Gracenote/TMS identifiers)
|
||||
- Solution: Search TMDb by title + release year
|
||||
- First result is used (good enough for most cases)
|
||||
|
||||
### Why No Authentication?
|
||||
- Channels DVR API has no auth (local network only)
|
||||
- Simplifies implementation
|
||||
- Security via network isolation
|
||||
|
||||
### Why Simplified Scanner?
|
||||
- Channels DVR doesn't expose resolution info via API
|
||||
- Defaults all content to non-4K
|
||||
- Future enhancement: parse video files for resolution
|
||||
|
||||
## What's NOT Done (Phase 2 & 3)
|
||||
|
||||
### Phase 2: UI Integration (TODO)
|
||||
- [ ] Settings page for Channels DVR URL configuration
|
||||
- [ ] Server connection test button
|
||||
- [ ] Library selection UI
|
||||
- [ ] Server type selector (Jellyfin/Plex/Emby/Channels DVR)
|
||||
|
||||
### Phase 3: Testing & Polish (TODO)
|
||||
- [ ] Test with real Channels DVR instance (http://192.168.0.15:8089)
|
||||
- [ ] Handle edge cases:
|
||||
- Shows/movies not found on TMDb
|
||||
- Network errors
|
||||
- Invalid URLs
|
||||
- [ ] Add proper error messages
|
||||
- [ ] Document configuration for users
|
||||
- [ ] Consider PR to upstream Seerr project
|
||||
|
||||
## Testing Instructions
|
||||
|
||||
### Prerequisites
|
||||
1. Channels DVR server running (http://192.168.0.15:8089)
|
||||
2. Seerr development environment set up
|
||||
3. Node.js + pnpm installed
|
||||
|
||||
### Manual Testing Steps
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
cd /home/node/.openclaw/workspace/seerr-explore
|
||||
pnpm install
|
||||
|
||||
# 2. Build the project
|
||||
pnpm build
|
||||
|
||||
# 3. Start Seerr
|
||||
pnpm start
|
||||
|
||||
# 4. Configure via UI:
|
||||
# - Go to Settings → Channels DVR
|
||||
# - Enter URL: http://192.168.0.15:8089
|
||||
# - Save
|
||||
|
||||
# 5. Trigger scan:
|
||||
# - Settings → Library Sync → Scan Channels DVR
|
||||
```
|
||||
|
||||
### API Testing (Without Full Seerr)
|
||||
```bash
|
||||
# Test Channels DVR API directly
|
||||
curl http://192.168.0.15:8089/api/v1/shows | jq '.[0]'
|
||||
curl http://192.168.0.15:8089/api/v1/movies | jq '.[0]'
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `server/constants/server.ts` - Added enum value
|
||||
- `server/api/channelsdvr.ts` - New API client
|
||||
- `server/lib/scanners/channelsdvr/index.ts` - New scanner
|
||||
- `server/lib/settings/index.ts` - Added settings interface
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Commit changes** to git
|
||||
2. **Test with real Channels DVR** instance
|
||||
3. **Build UI** for configuration (Phase 2)
|
||||
4. **Polish & document** (Phase 3)
|
||||
5. **Consider upstream PR** to Seerr project
|
||||
|
||||
## Notes
|
||||
|
||||
- Used Opus for architecture/planning phase
|
||||
- Downgraded to Sonnet for implementation details
|
||||
- Code follows existing Seerr patterns (Jellyfin scanner as reference)
|
||||
- TypeScript types are complete and match Channels DVR API
|
||||
- Ready for testing with real instance
|
||||
|
||||
## Resources
|
||||
|
||||
- Channels DVR API Docs: https://getchannels.com/docs/server-api/introduction/
|
||||
- Channels DVR Instance: http://192.168.0.15:8089
|
||||
- Seerr GitHub: https://github.com/seerr-team/seerr
|
||||
- Our Fork: https://git.bytesnap.io/ByteSnap/channels-seerr
|
||||
@@ -6,6 +6,12 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Automated AI-generated contributions without human review are not allowed and will be rejected.
|
||||
> This is an open-source project maintained by volunteers.
|
||||
> We do not have the resources to review pull requests that could have been avoided with proper human oversight.
|
||||
> While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards.
|
||||
> Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban.
|
||||
>
|
||||
> If you are using **any kind of AI assistance** to contribute to Seerr,
|
||||
> it must be disclosed in the pull request.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.1-alpine3.22@sha256:d28696cabe6a72c5addbb608b344818e5a158d849174abd4b1ae85ab48536280 AS base
|
||||
FROM node:22.22.0-alpine3.22@sha256:0c49915657c1c77c64c8af4d91d2f13fe96853bbd957993ed00dd592cbecc284 AS base
|
||||
ARG SOURCE_DATE_EPOCH
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
@@ -33,7 +33,7 @@ RUN pnpm build
|
||||
|
||||
RUN rm -rf .next/cache
|
||||
|
||||
FROM node:24.13.1-alpine3.22@sha256:d28696cabe6a72c5addbb608b344818e5a158d849174abd4b1ae85ab48536280
|
||||
FROM node:22.22.0-alpine3.22@sha256:0c49915657c1c77c64c8af4d91d2f13fe96853bbd957993ed00dd592cbecc284
|
||||
ARG SOURCE_DATE_EPOCH
|
||||
ARG COMMIT_TAG
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.1-alpine3.22@sha256:d28696cabe6a72c5addbb608b344818e5a158d849174abd4b1ae85ab48536280
|
||||
FROM node:22.22.0-alpine3.22@sha256:0c49915657c1c77c64c8af4d91d2f13fe96853bbd957993ed00dd592cbecc284
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
62
README-TESTING.md
Normal file
62
README-TESTING.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Testing Channels-Seerr with Docker
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Build and run:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.test.yml up --build
|
||||
```
|
||||
|
||||
2. **Access the web UI:**
|
||||
- Open browser: http://localhost:5055
|
||||
- Complete the setup wizard
|
||||
- Add your Channels DVR server in Settings
|
||||
|
||||
3. **Stop:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.test.yml down
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Config directory:** `./config` (created automatically, persists settings)
|
||||
- **Logs:** `docker-compose logs -f seerr`
|
||||
- **Port:** Default 5055 (change in docker-compose.test.yml if needed)
|
||||
|
||||
## Testing Channels DVR Integration
|
||||
|
||||
1. Start Seerr container
|
||||
2. Navigate to Settings → Channels DVR
|
||||
3. Add your Channels DVR server:
|
||||
- **Server URL:** http://your-channels-server:8089
|
||||
- **Test connection** to verify
|
||||
4. Enable sync jobs (manual or scheduled)
|
||||
5. Check logs for sync activity:
|
||||
```bash
|
||||
docker-compose -f docker-compose.test.yml logs -f seerr | grep -i channels
|
||||
```
|
||||
|
||||
## Development Testing
|
||||
|
||||
For faster iteration without full rebuilds:
|
||||
|
||||
```bash
|
||||
# Use Dockerfile.local for development
|
||||
docker build -f Dockerfile.local -t channels-seerr:dev .
|
||||
docker run -p 5055:5055 -v ./config:/app/config channels-seerr:dev
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Build fails:**
|
||||
- Check Node.js version (requires 22.x)
|
||||
- Try: `docker-compose -f docker-compose.test.yml build --no-cache`
|
||||
|
||||
**Can't connect to Channels DVR:**
|
||||
- If Channels is on host machine: Use `http://host.docker.internal:8089`
|
||||
- If on tailnet: Use the Tailscale IP
|
||||
- Check firewall allows connections from Docker network
|
||||
|
||||
**Database issues:**
|
||||
- SQLite (default): Stored in `./config/db/db.sqlite3`
|
||||
- To use Postgres: Uncomment postgres service in docker-compose.test.yml
|
||||
35
docker-compose.test.yml
Normal file
35
docker-compose.test.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
seerr:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
COMMIT_TAG: channels-dvr-test
|
||||
container_name: channels-seerr-test
|
||||
hostname: seerr
|
||||
ports:
|
||||
- "5055:5055"
|
||||
environment:
|
||||
- LOG_LEVEL=debug
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
restart: unless-stopped
|
||||
|
||||
# Optional: PostgreSQL for production-like testing
|
||||
# Uncomment if you want to test with Postgres instead of SQLite
|
||||
# postgres:
|
||||
# image: postgres:15-alpine
|
||||
# container_name: seerr-postgres
|
||||
# environment:
|
||||
# - POSTGRES_PASSWORD=seerr
|
||||
# - POSTGRES_USER=seerr
|
||||
# - POSTGRES_DB=seerr
|
||||
# volumes:
|
||||
# - postgres-data:/var/lib/postgresql/data
|
||||
# restart: unless-stopped
|
||||
|
||||
# volumes:
|
||||
# postgres-data:
|
||||
@@ -173,7 +173,7 @@
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.0.0",
|
||||
"node": "^22.0.0",
|
||||
"pnpm": "^10.0.0"
|
||||
},
|
||||
"config": {
|
||||
|
||||
234
pnpm-lock.yaml
generated
234
pnpm-lock.yaml
generated
@@ -3148,6 +3148,14 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.54.0':
|
||||
resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.54.0
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/parser@7.18.0':
|
||||
resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
@@ -3158,10 +3166,33 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/parser@8.54.0':
|
||||
resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.54.0':
|
||||
resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@7.18.0':
|
||||
resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
|
||||
'@typescript-eslint/scope-manager@8.54.0':
|
||||
resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.54.0':
|
||||
resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/type-utils@7.18.0':
|
||||
resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
@@ -3172,6 +3203,13 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/type-utils@8.54.0':
|
||||
resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/types@5.45.0':
|
||||
resolution: {integrity: sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -3180,6 +3218,10 @@ packages:
|
||||
resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
|
||||
'@typescript-eslint/types@8.54.0':
|
||||
resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@5.45.0':
|
||||
resolution: {integrity: sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -3198,12 +3240,25 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.54.0':
|
||||
resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/utils@7.18.0':
|
||||
resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
|
||||
'@typescript-eslint/utils@8.54.0':
|
||||
resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@5.45.0':
|
||||
resolution: {integrity: sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -3212,6 +3267,10 @@ packages:
|
||||
resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.54.0':
|
||||
resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
@@ -4624,6 +4683,10 @@ packages:
|
||||
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
eslint-visitor-keys@4.2.1:
|
||||
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
eslint@8.57.1:
|
||||
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -5014,22 +5077,20 @@ packages:
|
||||
glob@10.3.10:
|
||||
resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
global-directory@4.0.1:
|
||||
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
|
||||
@@ -5257,6 +5318,10 @@ packages:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
ignore@7.0.5:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
image-size@1.2.1:
|
||||
resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
|
||||
engines: {node: '>=16.x'}
|
||||
@@ -5600,6 +5665,10 @@ packages:
|
||||
resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
jiti@1.21.6:
|
||||
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
|
||||
hasBin: true
|
||||
|
||||
jiti@1.21.7:
|
||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
hasBin: true
|
||||
@@ -6241,6 +6310,10 @@ packages:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@9.0.4:
|
||||
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -6901,7 +6974,6 @@ packages:
|
||||
prebuild-install@7.1.3:
|
||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||
hasBin: true
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
@@ -8005,7 +8077,7 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
|
||||
|
||||
temp-dir@2.0.0:
|
||||
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
|
||||
@@ -8144,6 +8216,12 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
|
||||
ts-api-utils@2.4.0:
|
||||
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||
engines: {node: '>=18.12'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
@@ -12336,6 +12414,22 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/type-utils': 8.54.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
eslint: 8.57.1
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.4.0(typescript@5.4.5)
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.18.0
|
||||
@@ -12349,11 +12443,41 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.4.5)
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
eslint: 8.57.1
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.54.0(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.4.5)
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@7.18.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
|
||||
'@typescript-eslint/scope-manager@8.54.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.4.5)':
|
||||
dependencies:
|
||||
typescript: 5.4.5
|
||||
|
||||
'@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5)
|
||||
@@ -12366,10 +12490,24 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/type-utils@8.54.0(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.4.5)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
eslint: 8.57.1
|
||||
ts-api-utils: 2.4.0(typescript@5.4.5)
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@5.45.0': {}
|
||||
|
||||
'@typescript-eslint/types@7.18.0': {}
|
||||
|
||||
'@typescript-eslint/types@8.54.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@5.45.0(typescript@4.9.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.45.0
|
||||
@@ -12399,6 +12537,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.54.0(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.54.0(typescript@5.4.5)
|
||||
'@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.4.5)
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.4.5)
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
|
||||
@@ -12410,6 +12563,17 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/utils@8.54.0(eslint@8.57.1)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.4.5)
|
||||
eslint: 8.57.1
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@5.45.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.45.0
|
||||
@@ -12420,6 +12584,11 @@ snapshots:
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.54.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
JSONStream@1.3.5:
|
||||
@@ -13348,7 +13517,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.10.5
|
||||
cosmiconfig: 9.0.0(typescript@5.4.5)
|
||||
jiti: 1.21.7
|
||||
jiti: 1.21.6
|
||||
typescript: 5.4.5
|
||||
optional: true
|
||||
|
||||
@@ -13980,12 +14149,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 14.2.35
|
||||
'@rushstack/eslint-patch': 1.10.3
|
||||
'@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
'@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.1)
|
||||
@@ -14007,13 +14176,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
enhanced-resolve: 5.17.0
|
||||
eslint: 8.57.1
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.13.0
|
||||
is-core-module: 2.16.1
|
||||
@@ -14024,14 +14193,24 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.4.5)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14053,7 +14232,7 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-jest
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1):
|
||||
dependencies:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlastindex: 1.2.5
|
||||
@@ -14063,7 +14242,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -14142,6 +14321,8 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@3.4.3: {}
|
||||
|
||||
eslint-visitor-keys@4.2.1: {}
|
||||
|
||||
eslint@8.57.1:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
|
||||
@@ -14680,7 +14861,7 @@ snapshots:
|
||||
dependencies:
|
||||
foreground-child: 3.2.1
|
||||
jackspeak: 2.3.6
|
||||
minimatch: 9.0.5
|
||||
minimatch: 9.0.4
|
||||
minipass: 7.1.2
|
||||
path-scurry: 1.11.1
|
||||
|
||||
@@ -14688,7 +14869,7 @@ snapshots:
|
||||
dependencies:
|
||||
foreground-child: 3.2.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minimatch: 9.0.4
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
@@ -14979,6 +15160,8 @@ snapshots:
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
image-size@1.2.1:
|
||||
dependencies:
|
||||
queue: 6.0.2
|
||||
@@ -15327,6 +15510,9 @@ snapshots:
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
jiti@1.21.6:
|
||||
optional: true
|
||||
|
||||
jiti@1.21.7: {}
|
||||
|
||||
joi@17.13.3:
|
||||
@@ -16223,6 +16409,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.4:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -18366,6 +18556,10 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.4.5
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.4.5):
|
||||
dependencies:
|
||||
typescript: 5.4.5
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-node@10.9.2(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.5.1)(typescript@5.4.5):
|
||||
|
||||
220
server/api/channelsdvr.ts
Normal file
220
server/api/channelsdvr.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import logger from '@server/logger';
|
||||
|
||||
export interface ChannelsDVRShow {
|
||||
id: string;
|
||||
name: string;
|
||||
summary: string;
|
||||
image_url: string;
|
||||
release_year: number;
|
||||
release_date: string;
|
||||
genres: string[];
|
||||
categories: string[];
|
||||
labels: string[];
|
||||
cast: string[];
|
||||
episode_count: number;
|
||||
number_unwatched: number;
|
||||
favorited: boolean;
|
||||
last_watched_at?: number;
|
||||
last_recorded_at?: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface ChannelsDVRMovie {
|
||||
id: string;
|
||||
program_id: string;
|
||||
path: string;
|
||||
channel: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
full_summary: string;
|
||||
content_rating: string;
|
||||
image_url: string;
|
||||
thumbnail_url: string;
|
||||
duration: number;
|
||||
playback_time: number;
|
||||
release_year: number;
|
||||
release_date: string;
|
||||
genres: string[];
|
||||
tags: string[];
|
||||
labels: string[];
|
||||
categories: string[];
|
||||
cast: string[];
|
||||
directors: string[];
|
||||
watched: boolean;
|
||||
favorited: boolean;
|
||||
delayed: boolean;
|
||||
cancelled: boolean;
|
||||
corrupted: boolean;
|
||||
completed: boolean;
|
||||
processed: boolean;
|
||||
verified: boolean;
|
||||
last_watched_at?: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface ChannelsDVREpisode {
|
||||
id: string;
|
||||
show_id: string;
|
||||
program_id: string;
|
||||
path: string;
|
||||
channel: string;
|
||||
season_number: number;
|
||||
episode_number: number;
|
||||
title: string;
|
||||
episode_title: string;
|
||||
summary: string;
|
||||
full_summary: string;
|
||||
content_rating: string;
|
||||
image_url: string;
|
||||
thumbnail_url: string;
|
||||
duration: number;
|
||||
playback_time: number;
|
||||
original_air_date: string;
|
||||
genres: string[];
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
cast: string[];
|
||||
commercials: number[];
|
||||
watched: boolean;
|
||||
favorited: boolean;
|
||||
delayed: boolean;
|
||||
cancelled: boolean;
|
||||
corrupted: boolean;
|
||||
completed: boolean;
|
||||
processed: boolean;
|
||||
locked: boolean;
|
||||
verified: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
class ChannelsDVRAPI extends ExternalAPI {
|
||||
constructor(baseUrl: string) {
|
||||
super(
|
||||
baseUrl,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': `Seerr/${getAppVersion()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all TV shows from Channels DVR library
|
||||
*/
|
||||
public async getShows(): Promise<ChannelsDVRShow[]> {
|
||||
try {
|
||||
const data = await this.get<ChannelsDVRShow[]>('/api/v1/shows');
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Failed to fetch shows from Channels DVR', {
|
||||
label: 'Channels DVR API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
throw new Error('Failed to fetch shows from Channels DVR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific show by ID
|
||||
*/
|
||||
public async getShow(showId: string): Promise<ChannelsDVRShow> {
|
||||
try {
|
||||
const data = await this.get<ChannelsDVRShow>(`/api/v1/shows/${showId}`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to fetch show ${showId} from Channels DVR`,
|
||||
{
|
||||
label: 'Channels DVR API',
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
throw new Error(`Failed to fetch show ${showId} from Channels DVR`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all episodes for a specific show
|
||||
*/
|
||||
public async getShowEpisodes(showId: string): Promise<ChannelsDVREpisode[]> {
|
||||
try {
|
||||
const data = await this.get<ChannelsDVREpisode[]>(
|
||||
`/api/v1/shows/${showId}/episodes`
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to fetch episodes for show ${showId} from Channels DVR`,
|
||||
{
|
||||
label: 'Channels DVR API',
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to fetch episodes for show ${showId} from Channels DVR`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all movies from Channels DVR library
|
||||
*/
|
||||
public async getMovies(): Promise<ChannelsDVRMovie[]> {
|
||||
try {
|
||||
const data = await this.get<ChannelsDVRMovie[]>('/api/v1/movies');
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Failed to fetch movies from Channels DVR', {
|
||||
label: 'Channels DVR API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
throw new Error('Failed to fetch movies from Channels DVR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific movie by ID
|
||||
*/
|
||||
public async getMovie(movieId: string): Promise<ChannelsDVRMovie> {
|
||||
try {
|
||||
const data = await this.get<ChannelsDVRMovie>(`/api/v1/movies/${movieId}`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to fetch movie ${movieId} from Channels DVR`,
|
||||
{
|
||||
label: 'Channels DVR API',
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
throw new Error(`Failed to fetch movie ${movieId} from Channels DVR`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connectivity to Channels DVR server
|
||||
*/
|
||||
public async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
// Try to fetch shows list as a connectivity test
|
||||
await this.getShows();
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error('Channels DVR connection test failed', {
|
||||
label: 'Channels DVR API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelsDVRAPI;
|
||||
@@ -2,6 +2,7 @@ export enum MediaServerType {
|
||||
PLEX = 1,
|
||||
JELLYFIN,
|
||||
EMBY,
|
||||
CHANNELS_DVR,
|
||||
NOT_CONFIGURED,
|
||||
}
|
||||
|
||||
|
||||
305
server/lib/scanners/channelsdvr/index.ts
Normal file
305
server/lib/scanners/channelsdvr/index.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import ChannelsDVRAPI, {
|
||||
type ChannelsDVRMovie,
|
||||
type ChannelsDVRShow,
|
||||
} from '@server/api/channelsdvr';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import BaseScanner from '@server/lib/scanners/baseScanner';
|
||||
import type {
|
||||
ProcessableSeason,
|
||||
RunnableScanner,
|
||||
StatusBase,
|
||||
} from '@server/lib/scanners/baseScanner';
|
||||
import type { Library } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
|
||||
interface ChannelsDVRSyncStatus extends StatusBase {
|
||||
currentLibrary?: Library;
|
||||
libraries: Library[];
|
||||
}
|
||||
|
||||
class ChannelsDVRScanner
|
||||
extends BaseScanner<ChannelsDVRMovie | ChannelsDVRShow>
|
||||
implements RunnableScanner<ChannelsDVRSyncStatus>
|
||||
{
|
||||
private channelsClient: ChannelsDVRAPI;
|
||||
private libraries: Library[];
|
||||
private currentLibrary?: Library;
|
||||
private isRecentOnly = false;
|
||||
|
||||
constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
|
||||
super('Channels DVR Sync');
|
||||
this.isRecentOnly = isRecentOnly ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find TMDb ID for a movie by searching title and year
|
||||
*/
|
||||
private async findMovieTmdbId(
|
||||
title: string,
|
||||
releaseYear: number
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
// Clean up title (remove year suffix if present)
|
||||
const cleanTitle = title.replace(/\s*\(\d{4}\)\s*$/, '').trim();
|
||||
|
||||
this.log(
|
||||
`Searching TMDb for movie: "${cleanTitle}" (${releaseYear})`,
|
||||
'debug'
|
||||
);
|
||||
|
||||
const searchResults = await this.tmdb.searchMovies({
|
||||
query: cleanTitle,
|
||||
page: 1,
|
||||
year: releaseYear,
|
||||
});
|
||||
|
||||
if (searchResults.results.length === 0) {
|
||||
this.log(
|
||||
`No TMDb results found for movie: "${cleanTitle}" (${releaseYear})`,
|
||||
'warn'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the first result
|
||||
const tmdbId = searchResults.results[0].id;
|
||||
this.log(
|
||||
`Found TMDb ID ${tmdbId} for movie: "${cleanTitle}" (${releaseYear})`,
|
||||
'debug'
|
||||
);
|
||||
return tmdbId;
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Error searching TMDb for movie: "${title}" (${releaseYear})`,
|
||||
'error',
|
||||
{ errorMessage: e.message }
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find TMDb ID for a TV show by searching name and year
|
||||
*/
|
||||
private async findShowTmdbId(
|
||||
name: string,
|
||||
releaseYear: number
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
this.log(`Searching TMDb for show: "${name}" (${releaseYear})`, 'debug');
|
||||
|
||||
const searchResults = await this.tmdb.searchTvShows({
|
||||
query: name,
|
||||
page: 1,
|
||||
firstAirDateYear: releaseYear,
|
||||
});
|
||||
|
||||
if (searchResults.results.length === 0) {
|
||||
this.log(
|
||||
`No TMDb results found for show: "${name}" (${releaseYear})`,
|
||||
'warn'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the first result
|
||||
const tmdbId = searchResults.results[0].id;
|
||||
this.log(
|
||||
`Found TMDb ID ${tmdbId} for show: "${name}" (${releaseYear})`,
|
||||
'debug'
|
||||
);
|
||||
return tmdbId;
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Error searching TMDb for show: "${name}" (${releaseYear})`,
|
||||
'error',
|
||||
{ errorMessage: e.message }
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Channels DVR movie
|
||||
*/
|
||||
private async processChannelsDVRMovie(movie: ChannelsDVRMovie) {
|
||||
try {
|
||||
// Find TMDb ID by searching title and year
|
||||
const tmdbId = await this.findMovieTmdbId(
|
||||
movie.title,
|
||||
movie.release_year
|
||||
);
|
||||
|
||||
if (!tmdbId) {
|
||||
this.log(
|
||||
`Skipping movie "${movie.title}" - could not find TMDb ID`,
|
||||
'warn'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Channels DVR doesn't provide resolution info in the API
|
||||
// We'll default to non-4K for now
|
||||
const mediaAddedAt = new Date(movie.created_at);
|
||||
|
||||
await this.processMovie(tmdbId, {
|
||||
is4k: false,
|
||||
mediaAddedAt,
|
||||
ratingKey: movie.id,
|
||||
title: movie.title,
|
||||
serviceId: this.channelsClient.baseUrl,
|
||||
externalServiceId: this.channelsClient.baseUrl,
|
||||
externalServiceSlug: 'channelsdvr',
|
||||
tmdbId: tmdbId,
|
||||
processing: !movie.completed,
|
||||
});
|
||||
|
||||
this.log(`Processed movie: ${movie.title} (TMDb ID: ${tmdbId})`, 'info');
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Error processing Channels DVR movie: ${movie.title}`,
|
||||
'error',
|
||||
{
|
||||
errorMessage: e.message,
|
||||
movieId: movie.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Channels DVR TV show
|
||||
*/
|
||||
private async processChannelsDVRShow(show: ChannelsDVRShow) {
|
||||
try {
|
||||
// Find TMDb ID by searching name and year
|
||||
const tmdbId = await this.findShowTmdbId(show.name, show.release_year);
|
||||
|
||||
if (!tmdbId) {
|
||||
this.log(
|
||||
`Skipping show "${show.name}" - could not find TMDb ID`,
|
||||
'warn'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaAddedAt = new Date(show.created_at);
|
||||
|
||||
// Fetch all episodes for the show from Channels DVR
|
||||
const episodes = await this.channelsClient.getShowEpisodes(show.id);
|
||||
|
||||
// Group episodes by season
|
||||
const seasonMap = new Map<number, ProcessableSeason>();
|
||||
|
||||
for (const episode of episodes) {
|
||||
const seasonNumber = episode.season_number;
|
||||
const episodeNumber = episode.episode_number;
|
||||
|
||||
if (!seasonMap.has(seasonNumber)) {
|
||||
seasonMap.set(seasonNumber, {
|
||||
seasonNumber,
|
||||
episodes: [],
|
||||
});
|
||||
}
|
||||
|
||||
const season = seasonMap.get(seasonNumber)!;
|
||||
season.episodes.push({
|
||||
episodeNumber,
|
||||
ratingKey: episode.id,
|
||||
mediaAddedAt: new Date(episode.created_at),
|
||||
processing: !episode.completed,
|
||||
});
|
||||
}
|
||||
|
||||
const seasons = Array.from(seasonMap.values());
|
||||
|
||||
await this.processTvShow(tmdbId, {
|
||||
seasons,
|
||||
ratingKey: show.id,
|
||||
title: show.name,
|
||||
serviceId: this.channelsClient.baseUrl,
|
||||
externalServiceId: this.channelsClient.baseUrl,
|
||||
externalServiceSlug: 'channelsdvr',
|
||||
});
|
||||
|
||||
this.log(
|
||||
`Processed show: ${show.name} (TMDb ID: ${tmdbId}, ${episodes.length} episodes)`,
|
||||
'info'
|
||||
);
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Error processing Channels DVR show: ${show.name}`,
|
||||
'error',
|
||||
{
|
||||
errorMessage: e.message,
|
||||
showId: show.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
const sessionManager = settings.main.sessionManager;
|
||||
|
||||
if (!settings.channelsdvr.url) {
|
||||
this.log('Channels DVR URL not configured, skipping scan', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.channelsClient = new ChannelsDVRAPI(settings.channelsdvr.url);
|
||||
|
||||
// Test connection
|
||||
const connected = await this.channelsClient.testConnection();
|
||||
if (!connected) {
|
||||
throw new Error('Failed to connect to Channels DVR server');
|
||||
}
|
||||
|
||||
this.log('Successfully connected to Channels DVR', 'info');
|
||||
|
||||
// Fetch and process all movies
|
||||
this.log('Fetching movies from Channels DVR...', 'info');
|
||||
const movies = await this.channelsClient.getMovies();
|
||||
this.log(`Found ${movies.length} movies`, 'info');
|
||||
|
||||
for (const movie of movies) {
|
||||
await this.processChannelsDVRMovie(movie);
|
||||
}
|
||||
|
||||
// Fetch and process all TV shows
|
||||
this.log('Fetching TV shows from Channels DVR...', 'info');
|
||||
const shows = await this.channelsClient.getShows();
|
||||
this.log(`Found ${shows.length} TV shows`, 'info');
|
||||
|
||||
for (const show of shows) {
|
||||
await this.processChannelsDVRShow(show);
|
||||
}
|
||||
|
||||
this.log('Channels DVR sync completed', 'info');
|
||||
} catch (e) {
|
||||
this.log('Channels DVR sync failed', 'error', {
|
||||
errorMessage: e.message,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async cancel(): Promise<void> {
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
public status(): ChannelsDVRSyncStatus {
|
||||
return {
|
||||
running: this.running,
|
||||
progress: 0,
|
||||
total: 0,
|
||||
currentLibrary: this.currentLibrary,
|
||||
libraries: this.libraries ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelsDVRScanner;
|
||||
@@ -49,6 +49,13 @@ export interface JellyfinSettings {
|
||||
serverId: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface ChannelsDVRSettings {
|
||||
name: string;
|
||||
url: string;
|
||||
libraries: Library[];
|
||||
}
|
||||
|
||||
export interface TautulliSettings {
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
@@ -355,6 +362,7 @@ export interface AllSettings {
|
||||
main: MainSettings;
|
||||
plex: PlexSettings;
|
||||
jellyfin: JellyfinSettings;
|
||||
channelsdvr: ChannelsDVRSettings;
|
||||
tautulli: TautulliSettings;
|
||||
radarr: RadarrSettings[];
|
||||
sonarr: SonarrSettings[];
|
||||
@@ -423,6 +431,11 @@ class Settings {
|
||||
serverId: '',
|
||||
apiKey: '',
|
||||
},
|
||||
channelsdvr: {
|
||||
name: 'Channels DVR',
|
||||
url: '',
|
||||
libraries: [],
|
||||
},
|
||||
tautulli: {},
|
||||
metadataSettings: {
|
||||
tv: MetadataProviderType.TMDB,
|
||||
|
||||
Reference in New Issue
Block a user