@redseat/api 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Method, AxiosRequestConfig } from 'axios';
2
2
  import { Observable } from 'rxjs';
3
3
  import { IToken } from './auth.js';
4
4
  import { IServer } from './interfaces.js';
5
- import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSEPlayersListEvent } from './sse-types.js';
5
+ import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSEPlayersListEvent, SSEWatchedEvent, SSEUnwatchedEvent } from './sse-types.js';
6
6
  export interface ClientOptions {
7
7
  server: IServer;
8
8
  getIdToken: () => Promise<string>;
@@ -45,6 +45,8 @@ export declare class RedseatClient {
45
45
  readonly mediaRating$: Observable<SSEMediaRatingEvent>;
46
46
  readonly mediaProgress$: Observable<SSEMediaProgressEvent>;
47
47
  readonly playersList$: Observable<SSEPlayersListEvent>;
48
+ readonly watched$: Observable<SSEWatchedEvent>;
49
+ readonly unwatched$: Observable<SSEUnwatchedEvent>;
48
50
  /**
49
51
  * Creates a typed observable for a specific SSE event type.
50
52
  * Unwraps the nested data structure from the server (e.g., {uploadProgress: {...}} -> {...})
package/dist/client.js CHANGED
@@ -55,6 +55,8 @@ export class RedseatClient {
55
55
  this.mediaRating$ = this.createEventStream('media_rating');
56
56
  this.mediaProgress$ = this.createEventStream('media_progress');
57
57
  this.playersList$ = this.createEventStream('players-list');
58
+ this.watched$ = this.createEventStream('watched');
59
+ this.unwatched$ = this.createEventStream('unwatched');
58
60
  this.server = options.server;
59
61
  this.redseatUrl = options.redseatUrl;
60
62
  this.getIdToken = options.getIdToken;
@@ -483,6 +483,193 @@ export declare enum RsSort {
483
483
  Name = "name",
484
484
  Size = "size"
485
485
  }
486
+ /**
487
+ * Media type for history/watched tracking.
488
+ * Used to categorize watched entries and view progress.
489
+ */
490
+ export type MediaType = 'episode' | 'movie' | 'book' | 'song' | 'media';
491
+ /**
492
+ * Watch history entry - returned from GET /users/me/history.
493
+ *
494
+ * ## External ID Format
495
+ *
496
+ * Watch history uses **external IDs** (from providers like IMDb, Trakt, TMDb) rather than
497
+ * local database IDs. This design enables:
498
+ * - **Cross-server portability**: History syncs across different Redseat servers
499
+ * - **External service synchronization**: Seamless sync with Trakt, Plex, etc.
500
+ * - **Library independence**: History is global, not tied to a specific library
501
+ *
502
+ * ### ID Format: `provider:value`
503
+ *
504
+ * Examples:
505
+ * - `imdb:tt1234567` - IMDb ID for movies/episodes
506
+ * - `trakt:123456` - Trakt ID
507
+ * - `tmdb:550` - TMDb ID
508
+ * - `tvdb:12345` - TVDB ID for series/episodes
509
+ * - `redseat:abc123` - Local fallback when no external ID is available
510
+ *
511
+ * When marking content as watched via library endpoints (e.g., `setMovieWatched`),
512
+ * the server automatically resolves the local ID to an external ID.
513
+ */
514
+ export interface IWatched {
515
+ /** Content type (episode, movie, etc.) */
516
+ type: MediaType;
517
+ /**
518
+ * External ID in format `provider:value` (e.g., `imdb:tt1234567`).
519
+ * See interface documentation for details on the ID format.
520
+ */
521
+ id: string;
522
+ /** User reference (for admin queries) */
523
+ userRef?: string;
524
+ /** Timestamp when content was watched (unix milliseconds) */
525
+ date: number;
526
+ /** Timestamp when entry was last modified (unix milliseconds) */
527
+ modified: number;
528
+ }
529
+ /**
530
+ * Unwatched event data - emitted via SSE when content is removed from watch history.
531
+ *
532
+ * Unlike IWatched which has a single `id`, this uses `ids[]` array containing
533
+ * all possible external IDs (imdb, trakt, tmdb, local) so clients can match
534
+ * against any provider they have cached.
535
+ *
536
+ * This is a user-scoped event - not library-scoped.
537
+ */
538
+ export interface IUnwatched {
539
+ /** Content type (episode, movie, etc.) */
540
+ type: MediaType;
541
+ /**
542
+ * All possible external IDs for this content.
543
+ * Format: `provider:value`
544
+ * Examples: ["imdb:tt0111161", "trakt:481", "tmdb:278"]
545
+ */
546
+ ids: string[];
547
+ /** User reference */
548
+ userRef?: string;
549
+ /** Timestamp when entry was removed (unix milliseconds) */
550
+ modified: number;
551
+ }
552
+ /**
553
+ * Request body for adding to watch history via POST /users/me/history.
554
+ *
555
+ * ## External ID Requirement
556
+ *
557
+ * When using the direct history endpoint, you must provide an external ID.
558
+ * The ID format is `provider:value`:
559
+ * - `imdb:tt1234567` - IMDb ID
560
+ * - `trakt:123456` - Trakt ID
561
+ * - `tmdb:550` - TMDb ID
562
+ * - `tvdb:12345` - TVDB ID
563
+ *
564
+ * **Tip**: For easier usage, prefer the library-specific endpoints
565
+ * (`setMovieWatched`, `setEpisodeWatched`) which automatically resolve IDs.
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * // Direct history endpoint - requires external ID
570
+ * await serverApi.addToHistory({
571
+ * type: 'movie',
572
+ * id: 'imdb:tt0111161', // The Shawshank Redemption
573
+ * date: Date.now()
574
+ * });
575
+ *
576
+ * // Library endpoint - uses local ID, server resolves to external
577
+ * await libraryApi.setMovieWatched('local-movie-id', Date.now());
578
+ * ```
579
+ */
580
+ export interface IWatchedForAdd {
581
+ /** Content type (episode, movie, etc.) */
582
+ type: MediaType;
583
+ /**
584
+ * External ID in format `provider:value` (e.g., `imdb:tt1234567`).
585
+ * See interface documentation for details on the ID format.
586
+ */
587
+ id: string;
588
+ /** Timestamp when content was watched (unix milliseconds) */
589
+ date: number;
590
+ }
591
+ /**
592
+ * View progress entry - returned from GET /users/me/history/progress/:id.
593
+ *
594
+ * Tracks playback position for resumable viewing.
595
+ * Uses the same external ID format as watch history.
596
+ */
597
+ export interface IViewProgress {
598
+ /** Content type (episode, movie, etc.) */
599
+ type: MediaType;
600
+ /**
601
+ * External ID in format `provider:value` (e.g., `imdb:tt1234567`).
602
+ */
603
+ id: string;
604
+ /** User reference */
605
+ userRef: string;
606
+ /** Playback progress in milliseconds */
607
+ progress: number;
608
+ /** Parent ID for episodes (series external ID) */
609
+ parent?: string;
610
+ /** Timestamp when progress was last modified (unix milliseconds) */
611
+ modified: number;
612
+ }
613
+ /**
614
+ * Request body for updating view progress via POST /users/me/history/progress.
615
+ *
616
+ * Uses external IDs in format `provider:value` (e.g., `imdb:tt1234567`).
617
+ *
618
+ * @example
619
+ * ```typescript
620
+ * await serverApi.addProgress({
621
+ * type: 'movie',
622
+ * id: 'imdb:tt0111161',
623
+ * progress: 3600000 // 1 hour in milliseconds
624
+ * });
625
+ * ```
626
+ */
627
+ export interface IViewProgressForAdd {
628
+ /** Content type (episode, movie, etc.) */
629
+ type: MediaType;
630
+ /**
631
+ * External ID in format `provider:value` (e.g., `imdb:tt1234567`).
632
+ */
633
+ id: string;
634
+ /** Parent ID for episodes (series external ID) */
635
+ parent?: string;
636
+ /** Playback progress in milliseconds */
637
+ progress: number;
638
+ }
639
+ /**
640
+ * Query parameters for GET /users/me/history.
641
+ *
642
+ * @example
643
+ * ```typescript
644
+ * // Get recent movie history
645
+ * const history = await serverApi.getHistory({
646
+ * types: ['movie'],
647
+ * order: SqlOrder.DESC,
648
+ * after: Date.now() - 86400000 * 30 // Last 30 days
649
+ * });
650
+ *
651
+ * // Check if specific content was watched
652
+ * const watched = await serverApi.getHistory({
653
+ * id: 'imdb:tt0111161'
654
+ * });
655
+ * ```
656
+ */
657
+ export interface HistoryQuery {
658
+ /** Sort field */
659
+ sort?: RsSort;
660
+ /** Sort direction */
661
+ order?: SqlOrder;
662
+ /** Filter entries watched before this timestamp (unix ms) */
663
+ before?: number;
664
+ /** Filter entries watched after this timestamp (unix ms) */
665
+ after?: number;
666
+ /** Filter by content types */
667
+ types?: MediaType[];
668
+ /** Filter by specific external ID */
669
+ id?: string;
670
+ /** Pagination key for next page */
671
+ pageKey?: number;
672
+ }
486
673
  export type MovieSort = 'modified' | 'added' | 'created' | 'name' | 'digitalairdate';
487
674
  export interface DeletedQuery {
488
675
  after?: number;
package/dist/library.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Observable } from 'rxjs';
2
- import { IFile, ITag, IPerson, ISerie, IMovie, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, SerieInMedia, DeletedQuery, RsDeleted, MovieSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload } from './interfaces.js';
2
+ import { IFile, ITag, IPerson, ISerie, IMovie, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, SerieInMedia, DeletedQuery, RsDeleted, MovieSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched } from './interfaces.js';
3
3
  import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent, SSEMediaRatingEvent, SSEMediaProgressEvent } from './sse-types.js';
4
4
  import { EncryptFileOptions, EncryptedFile } from './encryption.js';
5
5
  export interface MediaForUpdate {
@@ -194,6 +194,9 @@ export declare class LibraryApi {
194
194
  updateMovie(movieId: string, updates: Partial<IMovie>): Promise<IMovie>;
195
195
  getMovieImages(movieId: string): Promise<ExternalImage[]>;
196
196
  setMovieWatched(movieId: string, date: number): Promise<void>;
197
+ getMovieWatched(movieId: string): Promise<IWatched>;
198
+ getMovieProgress(movieId: string): Promise<IViewProgress>;
199
+ setMovieProgress(movieId: string, progress: number): Promise<void>;
197
200
  searchMovies(name: string): Promise<IMovie[]>;
198
201
  movieRename(movieId: string, newName: string): Promise<IMovie>;
199
202
  updateMoviePoster(movieId: string, poster: FormData, type: string): Promise<void>;
@@ -214,6 +217,9 @@ export declare class LibraryApi {
214
217
  updatePersonPortrait(personId: string, portrait: FormData): Promise<void>;
215
218
  searchSeries(name: string): Promise<ISerie[]>;
216
219
  setEpisodeWatched(serieId: string, season: number, number: number, date: number): Promise<void>;
220
+ getEpisodeWatched(serieId: string, season: number, episode: number): Promise<IWatched>;
221
+ getEpisodeProgress(serieId: string, season: number, episode: number): Promise<IViewProgress>;
222
+ setEpisodeProgress(serieId: string, season: number, episode: number, progress: number): Promise<void>;
217
223
  /**
218
224
  * Searches for available media sources for a specific episode.
219
225
  * @param serieId - The series identifier
package/dist/library.js CHANGED
@@ -368,6 +368,17 @@ export class LibraryApi {
368
368
  async setMovieWatched(movieId, date) {
369
369
  await this.client.post(this.getUrl(`/movies/${movieId}/watched`), { date });
370
370
  }
371
+ async getMovieWatched(movieId) {
372
+ const res = await this.client.get(this.getUrl(`/movies/${movieId}/watched`));
373
+ return res.data;
374
+ }
375
+ async getMovieProgress(movieId) {
376
+ const res = await this.client.get(this.getUrl(`/movies/${movieId}/progress`));
377
+ return res.data;
378
+ }
379
+ async setMovieProgress(movieId, progress) {
380
+ await this.client.post(this.getUrl(`/movies/${movieId}/progress`), { progress });
381
+ }
371
382
  async searchMovies(name) {
372
383
  const res = await this.client.get(this.getUrl(`/movies/search?name=${name}`));
373
384
  return res.data;
@@ -460,6 +471,17 @@ export class LibraryApi {
460
471
  async setEpisodeWatched(serieId, season, number, date) {
461
472
  await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${number}/watched`), { date });
462
473
  }
474
+ async getEpisodeWatched(serieId, season, episode) {
475
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/watched`));
476
+ return res.data;
477
+ }
478
+ async getEpisodeProgress(serieId, season, episode) {
479
+ const res = await this.client.get(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/progress`));
480
+ return res.data;
481
+ }
482
+ async setEpisodeProgress(serieId, season, episode, progress) {
483
+ await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${episode}/progress`), { progress });
484
+ }
463
485
  /**
464
486
  * Searches for available media sources for a specific episode.
465
487
  * @param serieId - The series identifier
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RedseatClient } from './client.js';
2
- import { ILibrary, IPlugin, ICredential } from './interfaces.js';
2
+ import { ILibrary, IPlugin, ICredential, IWatched, IWatchedForAdd, IViewProgress, IViewProgressForAdd, HistoryQuery } from './interfaces.js';
3
3
  export declare class ServerApi {
4
4
  private client;
5
5
  constructor(client: RedseatClient);
@@ -19,4 +19,10 @@ export declare class ServerApi {
19
19
  deleteCredential(id: string): Promise<void>;
20
20
  saveOAuthCredentials(pluginId: string, params: Record<string, string>): Promise<void>;
21
21
  addLibraryCredential(credential: any): Promise<ILibrary>;
22
+ getHistory(query?: HistoryQuery): Promise<IWatched[]>;
23
+ addToHistory(watched: IWatchedForAdd): Promise<void>;
24
+ getProgressById(id: string): Promise<IViewProgress | null>;
25
+ addProgress(progress: IViewProgressForAdd): Promise<void>;
26
+ getAdminHistory(): Promise<IWatched[]>;
27
+ importHistory(watcheds: IWatchedForAdd[]): Promise<void>;
22
28
  }
package/dist/server.js CHANGED
@@ -48,4 +48,44 @@ export class ServerApi {
48
48
  const res = await this.client.post('/libraries/credential', credential);
49
49
  return res.data;
50
50
  }
51
+ // History methods
52
+ async getHistory(query) {
53
+ const params = {};
54
+ if (query) {
55
+ if (query.sort !== undefined)
56
+ params.sort = query.sort;
57
+ if (query.order !== undefined)
58
+ params.order = query.order;
59
+ if (query.before !== undefined)
60
+ params.before = query.before;
61
+ if (query.after !== undefined)
62
+ params.after = query.after;
63
+ if (query.types !== undefined && query.types.length > 0)
64
+ params.types = query.types;
65
+ if (query.id !== undefined)
66
+ params.id = query.id;
67
+ if (query.pageKey !== undefined)
68
+ params.pageKey = query.pageKey;
69
+ }
70
+ const res = await this.client.get('/users/me/history', { params });
71
+ return res.data;
72
+ }
73
+ async addToHistory(watched) {
74
+ await this.client.post('/users/me/history', watched);
75
+ }
76
+ async getProgressById(id) {
77
+ const res = await this.client.get(`/users/me/history/progress/${id}`);
78
+ return res.data;
79
+ }
80
+ async addProgress(progress) {
81
+ await this.client.post('/users/me/history/progress', progress);
82
+ }
83
+ // Admin history methods
84
+ async getAdminHistory() {
85
+ const res = await this.client.get('/users/admin/history');
86
+ return res.data;
87
+ }
88
+ async importHistory(watcheds) {
89
+ await this.client.post('/users/admin/history/import', watcheds);
90
+ }
51
91
  }
@@ -1,4 +1,4 @@
1
- import { ILibrary, IFile, IEpisode, ISerie, IMovie, IPerson, ITag, IBackup } from './interfaces.js';
1
+ import { ILibrary, IFile, IEpisode, ISerie, IMovie, IPerson, ITag, IBackup, IWatched, IUnwatched } from './interfaces.js';
2
2
  export type ElementAction = 'Added' | 'Updated' | 'Deleted';
3
3
  export type SSEConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
4
4
  export interface SSEConnectionOptions {
@@ -134,6 +134,18 @@ export interface SSEPlayersListEvent {
134
134
  userRef: string;
135
135
  players: SSEPlayerEvent[];
136
136
  }
137
+ /**
138
+ * SSE event emitted when content is marked as watched.
139
+ * This is a user-scoped event - not library-scoped.
140
+ * Uses external IDs (e.g., "imdb:tt0111161").
141
+ */
142
+ export type SSEWatchedEvent = IWatched;
143
+ /**
144
+ * SSE event emitted when content is removed from watch history.
145
+ * Contains all possible external IDs so clients can match against any cached ID.
146
+ * This is a user-scoped event - not library-scoped.
147
+ */
148
+ export type SSEUnwatchedEvent = IUnwatched;
137
149
  export interface SSEEventMap {
138
150
  'library': SSELibraryEvent;
139
151
  'library-status': SSELibraryStatusEvent;
@@ -150,6 +162,8 @@ export interface SSEEventMap {
150
162
  'media_rating': SSEMediaRatingEvent;
151
163
  'media_progress': SSEMediaProgressEvent;
152
164
  'players-list': SSEPlayersListEvent;
165
+ 'watched': SSEWatchedEvent;
166
+ 'unwatched': SSEUnwatchedEvent;
153
167
  }
154
168
  export type SSEEventName = keyof SSEEventMap;
155
169
  export interface SSEEvent<T extends SSEEventName = SSEEventName> {