Skip to content

Commit 2b3d156

Browse files
authored
Merge pull request #4 from neckaros/t3code/remove-trakt-forced-source
remove forced Trakt metadata source
2 parents 4e2278b + 44d57b5 commit 2b3d156

12 files changed

Lines changed: 1302 additions & 138 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Cargo.lock
99

1010
# These are backup files generated by rustfmt
1111
**/*.rs.bk
12+
__pycache__/
13+
*.pyc
1214

1315
# MSVC Windows builds of rustc generate these, which store debugging information
1416
*.pdb

docs/SSE.md

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -232,24 +232,20 @@ interface MediasRatingMessage {
232232
rating: Rating;
233233
}
234234

235-
// Watched events (user-specific)
236-
// IMPORTANT: The `id` field uses external IDs, NOT local database IDs.
237-
// Format: "provider:id" (e.g., "imdb:tt1234567", "trakt:123456", "tmdb:550")
238-
// For movies: Uses the best available external ID (priority: imdb > trakt > tmdb > tvdb > slug)
239-
// For episodes: Uses external IDs or falls back to local "redseat:{id}" if no external IDs exist
235+
// Watched events (user-specific). IDs include the media type and identity scheme.
240236
interface Watched {
241237
type: string; // MediaType: "movie", "episode", etc.
242-
id: string; // External ID in format "provider:value" (e.g., "imdb:tt1234567")
238+
id: string; // e.g. "movie:imdb/tt1234567" or "episode:redseat/seriesId/1/2"
243239
userRef?: string;
244240
date: number; // Timestamp when content was watched
245241
modified: number;
246242
}
247243

248244
// Unwatched events (user-specific)
249-
// NOTE: Different structure from Watched - contains ALL possible IDs for client matching
245+
// NOTE: Different structure from Watched because the delete API accepts multiple IDs.
250246
interface Unwatched {
251247
type: string; // MediaType: "movie", "episode", etc.
252-
ids: string[]; // All possible IDs in format "provider:value" (e.g., ["imdb:tt1234567", "trakt:12345", "tmdb:550"])
248+
ids: string[]; // History IDs marked as deleted
253249
userRef?: string;
254250
modified: number;
255251
}
@@ -364,7 +360,7 @@ eventSource.addEventListener('unwatched', (event) => {
364360
const data: SseEvent = JSON.parse(event.data);
365361
if ('Unwatched' in data) {
366362
const unwatched = data.Unwatched;
367-
// Unwatched events contain ALL possible IDs for the content
363+
// Unwatched events contain the history IDs marked as deleted
368364
console.log(`Unmarked as watched: ${unwatched.type} with IDs: ${unwatched.ids.join(', ')}`);
369365
}
370366
});
@@ -574,14 +570,14 @@ Search endpoints support SSE streaming so clients receive results progressively
574570

575571
Each SSE event has event type `results`. The data is a JSON object with a single key: the provider name, and the value is an array of results from that provider.
576572

577-
Results arrive one provider at a time. For series and movies, Trakt results are sent first, followed by each plugin (e.g., Anilist). For books, only plugin results are sent (no Trakt).
573+
Results arrive one metadata plugin at a time.
578574

579575
### Event Format
580576

581577
Each `results` event contains one provider's results:
582578

583579
```json
584-
{"trakt": [{"metadata": {"serie": { ... }}, "images": []}]}
580+
{"TMDB": [{"metadata": {"serie": { ... }}, "images": []}]}
585581
```
586582

587583
Then a second event for the next provider:
@@ -605,7 +601,7 @@ const resultsByProvider: Record<string, SearchResult[]> = {};
605601

606602
eventSource.addEventListener('results', (event) => {
607603
const data = JSON.parse(event.data);
608-
// data is e.g. { "trakt": [...] } or { "Anilist": [...] }
604+
// data is e.g. { "TMDB": [...] } or { "Anilist": [...] }
609605
for (const [provider, results] of Object.entries(data)) {
610606
resultsByProvider[provider] = results;
611607
}
@@ -634,7 +630,7 @@ Response format:
634630

635631
```json
636632
{
637-
"trakt": [{"metadata": {"movie": { ... }}, "images": []}],
633+
"TMDB": [{"metadata": {"movie": { ... }}, "images": []}],
638634
"Anilist": [{"metadata": {"movie": { ... }}, "images": [...]}]
639635
}
640636
```
@@ -651,22 +647,16 @@ When a client falls behind and misses events (lag), the server will skip the mis
651647

652648
### Understanding the ID Format
653649

654-
The `watched` and `unwatched` events use **external IDs** (from providers like IMDb, Trakt, TMDb) rather than local database IDs. This allows watch history to be portable across different servers and sync with external services.
650+
History IDs include the media type so IDs from different domains cannot collide.
655651

656-
**ID Format**: `provider:value`
652+
| Content | Format | Example |
653+
|---------|--------|---------|
654+
| Movie with IMDb ID | `movie:imdb/<imdbId>` | `movie:imdb/tt1234567` |
655+
| Movie without IMDb ID | `movie:redseat/<movieId>` | `movie:redseat/abc123` |
656+
| Series progress parent | `series:redseat/<seriesId>` | `series:redseat/series123` |
657+
| Episode | `episode:redseat/<seriesId>/<season>/<episode>` | `episode:redseat/series123/1/2` |
657658

658-
| Provider | Example | Content Types |
659-
|----------|---------|---------------|
660-
| `imdb` | `imdb:tt1234567` | Movies, Episodes |
661-
| `trakt` | `trakt:123456` | Movies, Episodes, Series |
662-
| `tmdb` | `tmdb:550` | Movies, Episodes, Series |
663-
| `tvdb` | `tvdb:78901` | Episodes, Series |
664-
| `slug` | `slug:the-matrix` | Movies, Series |
665-
| `redseat` | `redseat:abc123` | Local fallback (episodes only) |
666-
667-
**ID Selection Priority**:
668-
- **Movies**: Uses the best external ID (priority: imdb > trakt > tmdb > slug)
669-
- **Episodes**: Uses external IDs, or falls back to local `redseat:` ID if no external IDs exist
659+
Episode IDs deliberately use the immutable local series ID and numeric season/episode tuple. Plugin metadata refreshes therefore cannot change watched state or progress IDs.
670660

671661
### REST API Endpoints
672662

@@ -682,30 +672,32 @@ The `watched` and `unwatched` events use **external IDs** (from providers like I
682672
{ "date": 1705766400000 }
683673
```
684674

685-
**Direct History** (requires knowing the external ID): `POST /users/me/history`
675+
**Direct History**: `POST /users/me/history`
686676
```json
687677
{
688678
"type": "movie",
689-
"id": "imdb:tt1234567",
679+
"id": "movie:imdb/tt1234567",
690680
"date": 1705766400000
691681
}
692682
```
693683

684+
For movie compatibility, `imdb:tt1234567` is also accepted and normalized to the typed form. Other media should send the typed history ID returned by the history API or SSE event.
685+
694686
#### Unmark as Watched (Remove from History)
695687

696688
**Movies**: `DELETE /libraries/:libraryId/movies/:id/watched`
697689

698690
**Episodes**: `DELETE /libraries/:libraryId/series/:serieId/seasons/:season/episodes/:number/watched`
699691

700-
**Direct History** (with multiple possible IDs): `DELETE /users/me/history`
692+
**Direct History**: `DELETE /users/me/history`
701693
```json
702694
{
703695
"type": "movie",
704-
"ids": ["imdb:tt1234567", "trakt:12345", "tmdb:550"]
696+
"ids": ["movie:imdb/tt1234567"]
705697
}
706698
```
707699

708-
The delete endpoints accept multiple IDs because the watched entry could have been created with any of the available external IDs. The server will try to delete entries matching any of the provided IDs.
700+
The delete endpoint accepts an array so clients can delete more than one known history ID in one request.
709701

710702
### Example: Handling Watch State Changes
711703

@@ -737,25 +729,19 @@ eventSource.addEventListener('unwatched', (event) => {
737729

738730
### Matching SSE Events to Local Content
739731

740-
Since SSE events use external IDs, you need to match them against your local content's external IDs:
732+
Match SSE events against the same typed history ID used by the REST API:
741733

742734
```typescript
743735
interface LocalMovie {
744736
id: string; // Local database ID
745737
imdb?: string; // "tt1234567"
746-
trakt?: number; // 12345
747-
tmdb?: number; // 550
748738
}
749739

750-
// For Watched events (single ID)
751740
function isMatchingWatchedEvent(movie: LocalMovie, eventId: string): boolean {
752-
const [provider, value] = eventId.split(':');
753-
switch (provider) {
754-
case 'imdb': return movie.imdb === value;
755-
case 'trakt': return movie.trakt?.toString() === value;
756-
case 'tmdb': return movie.tmdb?.toString() === value;
757-
default: return false;
758-
}
741+
const historyId = movie.imdb
742+
? `movie:imdb/${movie.imdb}`
743+
: `movie:redseat/${movie.id}`;
744+
return eventId === historyId;
759745
}
760746

761747
// For Unwatched events (array of IDs)
@@ -824,14 +810,14 @@ syncHistory();
824810
[
825811
{
826812
"type": "movie",
827-
"id": "imdb:tt1234567",
813+
"id": "movie:imdb/tt1234567",
828814
"userRef": "user123",
829815
"date": 1705766400000,
830816
"modified": 1705852800000
831817
},
832818
{
833819
"type": "movie",
834-
"id": "trakt:98765",
820+
"id": "movie:imdb/tt9876543",
835821
"userRef": "user123",
836822
"date": 0,
837823
"modified": 1705939200000

0 commit comments

Comments
 (0)