@omelhorsite/sdk 0.2.0 → 0.3.0

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.
Files changed (44) hide show
  1. package/dist/index.js +4939 -552
  2. package/dist/types/client.d.ts +60 -3
  3. package/dist/types/http.d.ts +444 -19
  4. package/dist/types/index.d.ts +4 -1
  5. package/dist/types/resources/account.d.ts +66 -3
  6. package/dist/types/resources/admin.d.ts +1837 -0
  7. package/dist/types/resources/auth/index.d.ts +39 -0
  8. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  9. package/dist/types/resources/auth/sessions.d.ts +847 -0
  10. package/dist/types/resources/chests.d.ts +54 -3
  11. package/dist/types/resources/content.d.ts +2970 -0
  12. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  13. package/dist/types/resources/forms.d.ts +176 -35
  14. package/dist/types/resources/index.d.ts +19 -8
  15. package/dist/types/resources/ipLookup.d.ts +20 -4
  16. package/dist/types/resources/jobs.d.ts +62 -21
  17. package/dist/types/resources/library.d.ts +1435 -0
  18. package/dist/types/resources/linkTrees.d.ts +142 -30
  19. package/dist/types/resources/media.d.ts +351 -0
  20. package/dist/types/resources/movies.d.ts +1186 -0
  21. package/dist/types/resources/music/artists.d.ts +1066 -0
  22. package/dist/types/resources/music/imports.d.ts +940 -0
  23. package/dist/types/resources/music/index.d.ts +61 -0
  24. package/dist/types/resources/music/playlists.d.ts +1026 -0
  25. package/dist/types/resources/music/social.d.ts +1132 -0
  26. package/dist/types/resources/music/songs.d.ts +1183 -0
  27. package/dist/types/resources/notepads.d.ts +4 -1
  28. package/dist/types/resources/quotas.d.ts +7 -1
  29. package/dist/types/resources/realtime.d.ts +855 -0
  30. package/dist/types/resources/shortLinks.d.ts +45 -4
  31. package/dist/types/resources/social.d.ts +1330 -0
  32. package/dist/types/resources/storage/upload.d.ts +158 -11
  33. package/dist/types/resources/storage.d.ts +88 -22
  34. package/dist/types/resources/tickets.d.ts +82 -3
  35. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  36. package/dist/types/resources/tools/captions.d.ts +448 -21
  37. package/dist/types/resources/tools/downloader.d.ts +21 -0
  38. package/dist/types/resources/tools/index.d.ts +57 -15
  39. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  40. package/dist/types/resources/tools/transcription.d.ts +35 -13
  41. package/dist/types/resources/tools/upscale.d.ts +23 -3
  42. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  43. package/dist/types/types.d.ts +249 -17
  44. package/package.json +2 -1
@@ -0,0 +1,1186 @@
1
+ /**
2
+ * The `movies` namespace: the Stremio-style movie and series app.
3
+ *
4
+ * Six endpoint families live here because they are one app's worth of API:
5
+ * the addons that supply catalogues and streams, the groups and grants that
6
+ * share those addons with friends, the collections a user files titles into,
7
+ * and the watch progress that drives "Continuar a ver". They are exposed as
8
+ * one entry class ({@link MoviesNamespace}) with sub-namespaces hanging off it
9
+ * ({@link MoviesNamespace.addons}, `.addons.groups`, `.addons.grants`,
10
+ * `.collections`, `.collections.items`, `.watchProgress`), and every
11
+ * sub-namespace is also exported on its own so a host that prefers
12
+ * `oms.movieCollections` can mount it there instead.
13
+ *
14
+ * ## Seven things that have already cost bugs
15
+ *
16
+ * 1. **Every id here is an opaque 12-character STRING**, minted by
17
+ * `RandomIdentifier`, not an auto-increment integer like the music tables.
18
+ * They do not sort by creation and there is no arithmetic to do on one.
19
+ * 2. **The whole namespace requires a session.** `ApplicationController`
20
+ * requires authentication by default and none of the six controllers opts
21
+ * out, so an anonymous call is `401 "Session required to access this
22
+ * resource."` - not an empty list. Worse for OAuth: no movies controller
23
+ * declares an `oauth_scope`, and the scope gate denies by omission, so an
24
+ * OAuth access token gets `403 {"error":"insufficient_scope"}` on every
25
+ * route in this file. Cookie sessions and personal tokens work; third-party
26
+ * OAuth clients do not. See {@link MoviesNamespace}.
27
+ * 3. **`PATCH /movie_addons/:id` wipes `manifest_json` unless you resend it.**
28
+ * The controller assigns the key unconditionally, so a partial patch sends
29
+ * `nil` into a `presence: true` validation and answers `400`. That is why
30
+ * {@link UpdateMovieAddonInput.manifest_json} is REQUIRED here, and why
31
+ * {@link MovieAddonsNamespace.moveToGroup} exists.
32
+ * 4. **Listing collections has a side effect**: it is the only place that
33
+ * creates the user's "Favoritos" row. See
34
+ * {@link MovieCollectionsNamespace.list}.
35
+ * 5. **`POST /movie_watch_progresses` is an upsert that answers `200`, not
36
+ * `201`**, and its identity is `(user, movie_id, video_id)` - `movie_type`
37
+ * is NOT part of the key. It is one of the very few creates in the API that
38
+ * breaks the 201 convention, because it is not really a create.
39
+ * 6. **`finished` is a three-state field, not a boolean.** Omitted means "you
40
+ * decide from the position"; `true`/`false` means "the user said so". A
41
+ * `null` is deleted by the controller and reads as omitted, which is
42
+ * exactly the bug that once made "marcar como nao visto" silently do
43
+ * nothing. See {@link MovieWatchProgressInput.finished}.
44
+ * 7. **`last_watched_at` is only defaulted on INSERT.** The model does
45
+ * `self.last_watched_at ||= Time.current`, so an upsert onto an existing row
46
+ * that omits it keeps the OLD timestamp - and the Continue Watching list is
47
+ * ordered by exactly that column. Always send one. See
48
+ * {@link MovieWatchProgressInput.last_watched_at}.
49
+ *
50
+ * Everything here rides the general ceiling: **600 requests per minute** for an
51
+ * authenticated caller. There is no movie-specific rack-attack bucket, and
52
+ * since anonymous callers cannot reach any of it, the 120/min anonymous bucket
53
+ * never applies.
54
+ */
55
+ import { ApiClient, Resource } from "../http";
56
+ import type { BaseRecord, Id, PageParams, Paginated, RequestOptions, Timestamp } from "../types";
57
+ import type { User } from "./account";
58
+ /**
59
+ * What a title is. Stremio's own vocabulary, and the backend stores it as a
60
+ * free string with no inclusion validation, so an addon may invent one.
61
+ * Compare against this union for the cases you handle and fall through for the
62
+ * rest rather than assuming the list is closed.
63
+ */
64
+ export type MovieType = "movie" | "series" | "channel" | "tv" | (string & {});
65
+ /** The four resource names a Stremio manifest may advertise. */
66
+ export type StremioResourceName = "catalog" | "meta" | "stream" | "subtitles";
67
+ /** One catalogue an addon offers, as declared in its manifest. */
68
+ export interface StremioCatalog {
69
+ readonly type: string;
70
+ readonly id: string;
71
+ readonly name?: string;
72
+ readonly extra?: ReadonlyArray<{
73
+ readonly name: string;
74
+ readonly isRequired?: boolean;
75
+ readonly options?: readonly string[];
76
+ }>;
77
+ }
78
+ /**
79
+ * An addon's `manifest.json`, stored verbatim in a `jsonb` column.
80
+ *
81
+ * The backend does not validate a single key of it beyond "not blank": it is
82
+ * `params[:manifest_json].to_unsafe_h`, written straight to the column and read
83
+ * straight back. So the fields below are what a well-behaved Stremio addon
84
+ * sends, not a contract the server enforces - `id` and `name` can be missing
85
+ * on a hostile or broken manifest even though they are typed as required here,
86
+ * and the index signature is there because whatever else the addon declared
87
+ * round-trips untouched.
88
+ *
89
+ * Never trust `logo`, `background` or any URL inside one without checking the
90
+ * origin: this blob is user-supplied content that the app renders.
91
+ */
92
+ export interface StremioManifest {
93
+ readonly id: string;
94
+ readonly name: string;
95
+ readonly description?: string;
96
+ readonly version?: string;
97
+ readonly resources?: ReadonlyArray<StremioResourceName | {
98
+ readonly name: StremioResourceName;
99
+ readonly types?: readonly string[];
100
+ }>;
101
+ readonly types?: readonly string[];
102
+ readonly catalogs?: readonly StremioCatalog[];
103
+ readonly logo?: string;
104
+ readonly background?: string;
105
+ /** Anything else the manifest carried. `jsonb` keeps it all. */
106
+ readonly [key: string]: unknown;
107
+ }
108
+ /**
109
+ * An installed addon: a manifest URL plus the manifest fetched from it.
110
+ *
111
+ * Rows reach a caller two ways, and {@link MovieAddon.shared} is how you tell
112
+ * them apart: the ones the caller installed, and the ones somebody granted
113
+ * them (directly, or through a group). A shared addon is read-only in
114
+ * practice - `updatable_by?` and `destroyable_by?` both require ownership, so
115
+ * every write against one is a `401`.
116
+ */
117
+ export interface MovieAddon extends BaseRecord {
118
+ /** Owner. Compare against your own id, or just read {@link shared}. */
119
+ readonly user_id: Id;
120
+ /** Group the owner filed it under, or `null` when it is ungrouped. */
121
+ readonly movie_addon_group_id: Id | null;
122
+ /** Where the manifest was fetched from. Unique per owner. */
123
+ readonly manifest_url: string;
124
+ /** The manifest itself, stored verbatim. */
125
+ readonly manifest_json: StremioManifest;
126
+ /**
127
+ * `true` when this row belongs to somebody else and reached you through a
128
+ * grant. Computed against the CALLER, so the same row is `false` for its
129
+ * owner and `true` for everyone it is shared with - never cache it across
130
+ * identities.
131
+ */
132
+ readonly shared: boolean;
133
+ }
134
+ /** Arguments for {@link MovieAddonsNamespace.create}. */
135
+ export interface CreateMovieAddonInput {
136
+ /**
137
+ * Absolute `http`/`https` URL of the manifest. Validated with
138
+ * `URI::DEFAULT_PARSER.make_regexp`, so anything else is
139
+ * `400 "Manifest url must be a valid URL"`.
140
+ */
141
+ readonly manifest_url: string;
142
+ /**
143
+ * The fetched manifest. Required: `presence: true` rejects both `nil` and
144
+ * `{}`, so an addon whose manifest failed to download cannot be stored as a
145
+ * placeholder.
146
+ */
147
+ readonly manifest_json: StremioManifest;
148
+ /**
149
+ * Group to file it under. The group must belong to the SAME user, or the
150
+ * save fails with `400 "Movie addon group must belong to addon owner"`.
151
+ */
152
+ readonly movie_addon_group_id?: Id | null;
153
+ }
154
+ /**
155
+ * Arguments for {@link MovieAddonsNamespace.update}.
156
+ *
157
+ * `manifest_json` is required, and that is not an oversight to work around.
158
+ * `MovieAddonsController#movie_addon_params` is shared by create and update and
159
+ * ends with an unconditional `permitted[:manifest_json] = params[:manifest_json]`,
160
+ * so a PATCH that omits the key assigns `nil` over the stored manifest and then
161
+ * trips `validates :manifest_json, presence: true`. The row survives (the save
162
+ * failed) but the call is a `400`, which is how `moveAddonToGroup` in the web
163
+ * frontend is broken today - it sends `{ movie_addon_group_id }` alone. Resend
164
+ * the manifest you already hold; {@link MovieAddonsNamespace.moveToGroup} does
165
+ * it for you.
166
+ */
167
+ export interface UpdateMovieAddonInput {
168
+ /** Resend the stored manifest verbatim. See the interface docs for why. */
169
+ readonly manifest_json: StremioManifest;
170
+ /** `null` un-groups the addon. Omitting the key leaves the group alone. */
171
+ readonly movie_addon_group_id?: Id | null;
172
+ /**
173
+ * Only if you really are re-pointing the addon. Changing it can collide with
174
+ * the `(user_id, manifest_url)` unique index, which surfaces as
175
+ * `400 "Manifest url has already been taken"`.
176
+ */
177
+ readonly manifest_url?: string;
178
+ }
179
+ /**
180
+ * Filters for {@link MovieAddonsNamespace.list}.
181
+ *
182
+ * The server's allowlist for this index is exactly `user_id`, `id`,
183
+ * `created_at` and `updated_at`. Any other filter key is
184
+ * `400 "Unknown search filter: ..."` - filters fail closed rather than widening
185
+ * the query - so there is deliberately no way to list by
186
+ * `movie_addon_group_id`. Group client-side off the field on each row.
187
+ */
188
+ export interface ListMovieAddonsParams extends PageParams {
189
+ /** Exact match. Use it to split your own addons from the shared ones. */
190
+ readonly userId?: Id;
191
+ /** Exact match on the primary key. */
192
+ readonly id?: Id;
193
+ }
194
+ /** Arguments for {@link MovieAddonGroupsNamespace.create}. */
195
+ export interface CreateMovieAddonGroupInput {
196
+ /** Required, at most {@link MOVIE_ADDON_GROUP_NAME_MAX_LENGTH} characters. */
197
+ readonly name: string;
198
+ }
199
+ /**
200
+ * A named folder for addons, owned by one user.
201
+ *
202
+ * A group is also a sharing unit: granting a group shares every addon inside
203
+ * it, including ones added later, which is the whole reason groups exist.
204
+ */
205
+ export interface MovieAddonGroup extends BaseRecord {
206
+ readonly user_id: Id;
207
+ readonly name: string;
208
+ }
209
+ /** Filters for {@link MovieAddonGroupsNamespace.list}. */
210
+ export interface ListMovieAddonGroupsParams extends PageParams {
211
+ /** Exact match on the primary key. The only filter this index allows. */
212
+ readonly id?: Id;
213
+ }
214
+ /**
215
+ * A share: one addon, or one whole group, handed to one other user.
216
+ *
217
+ * Exactly one of `movie_addon_id` and `movie_addon_group_id` is set; the other
218
+ * is `null`. A database check constraint enforces it as well as the model, so
219
+ * there is no path to a row with both or neither.
220
+ */
221
+ export interface MovieAddonGrant extends BaseRecord {
222
+ /** Set when this grant targets a single addon. */
223
+ readonly movie_addon_id: Id | null;
224
+ /** Set when this grant targets a whole group. */
225
+ readonly movie_addon_group_id: Id | null;
226
+ /** Who shared. Always the caller for a grant you created. */
227
+ readonly grantor_id: Id;
228
+ /** Who received. */
229
+ readonly grantee_id: Id;
230
+ /**
231
+ * The grantee, rendered as a full user.
232
+ *
233
+ * The web frontend types this as `{ id, name, handle }`; that is a subset,
234
+ * not the payload. `MovieAddonGrantBlueprint` does
235
+ * `JSON.parse(grant.grantee.render)`, which is `UserBlueprint`'s DEFAULT view
236
+ * with `Current.user` injected as the viewer - so `bio`, `country_code`, the
237
+ * `library_*` fields and the visibility flags all come along, and `email` /
238
+ * `gender` / `group` appear or not depending on who is asking.
239
+ *
240
+ * There is no matching `grantor` field: you only ever see grants you made or
241
+ * received, so the other side is either you or the grantee.
242
+ */
243
+ readonly grantee: User;
244
+ }
245
+ /**
246
+ * Arguments for {@link MovieAddonGrantsNamespace.create}.
247
+ *
248
+ * Pass exactly one target. Both or neither is
249
+ * `400 "Grant must target one addon or one group"`.
250
+ */
251
+ export interface CreateMovieAddonGrantInput {
252
+ /** Share one addon. Mutually exclusive with {@link movie_addon_group_id}. */
253
+ readonly movie_addon_id?: Id | null;
254
+ /** Share a whole group, present and future contents. */
255
+ readonly movie_addon_group_id?: Id | null;
256
+ /** Who to share with. Must not be yourself. */
257
+ readonly grantee_id: Id;
258
+ }
259
+ /**
260
+ * Filters for {@link MovieAddonGrantsNamespace.list}.
261
+ *
262
+ * This controller declares no `search_params` at all, so the allowlist is only
263
+ * the framework default: `id`, `created_at`, `updated_at`. You CANNOT ask the
264
+ * server for "grants I made" versus "grants I received", nor for the grants on
265
+ * one addon - those are `400`s. Filter on `grantor_id` / `grantee_id` /
266
+ * `movie_addon_id` client-side after listing.
267
+ */
268
+ export interface ListMovieAddonGrantsParams extends PageParams {
269
+ /** Exact match on the primary key. */
270
+ readonly id?: Id;
271
+ }
272
+ /** The manual kind: a collection the user created and may rename or delete. */
273
+ export declare const MOVIE_COLLECTION_MANUAL_KIND = "manual";
274
+ /**
275
+ * The one system kind. There is exactly one favourites collection per user,
276
+ * enforced by a partial unique index on `(user_id, kind) WHERE kind <> 'manual'`.
277
+ */
278
+ export declare const MOVIE_COLLECTION_FAVORITES_KIND = "favorites";
279
+ /** Every kind `MovieCollection::KINDS` allows. Anything else is a `400`. */
280
+ export declare const MOVIE_COLLECTION_KINDS: readonly ["manual", "favorites"];
281
+ /** One of {@link MOVIE_COLLECTION_KINDS}. */
282
+ export type MovieCollectionKind = (typeof MOVIE_COLLECTION_KINDS)[number];
283
+ /** Longest name `MovieAddonGroup` accepts. Over it is a `400`. */
284
+ export declare const MOVIE_ADDON_GROUP_NAME_MAX_LENGTH = 80;
285
+ /**
286
+ * A user's list of titles: the auto-created favourites row, or a playlist they
287
+ * made by hand.
288
+ */
289
+ export interface MovieCollection extends BaseRecord {
290
+ readonly user_id: Id;
291
+ readonly name: string;
292
+ readonly kind: MovieCollectionKind;
293
+ /**
294
+ * Sort key inside the sidebar. Favourites is minted at `-1` so it sorts
295
+ * first; manual collections start at `max + 1`.
296
+ *
297
+ * The index has NO order of its own, so this only sorts if you ask for it -
298
+ * pass `order: "position:asc"`.
299
+ */
300
+ readonly position: number;
301
+ /**
302
+ * `true` for the favourites row. Mirrors `kind != "manual"`, so it is `true`
303
+ * for any future system kind too. Use {@link isSystemMovieCollection}.
304
+ */
305
+ readonly system: boolean;
306
+ /**
307
+ * How many items are in it. Counted from the association the controller
308
+ * preloads, so it is exact and costs no extra query - but it is a snapshot,
309
+ * and adding an item does not refresh the collection row you are holding.
310
+ */
311
+ readonly items_count: number;
312
+ }
313
+ /** One title filed into a collection. */
314
+ export interface MovieCollectionItem extends BaseRecord {
315
+ readonly movie_collection_id: Id;
316
+ /** `"movie"`, `"series"`, whatever the addon called it. */
317
+ readonly movie_type: MovieType;
318
+ /** The addon's own id for the title, e.g. an IMDb id. Not a database id. */
319
+ readonly movie_id: string;
320
+ /** Denormalised metadata, so a grid renders without hitting the addon. */
321
+ readonly name: string | null;
322
+ readonly poster: string | null;
323
+ readonly background: string | null;
324
+ readonly release_info: string | null;
325
+ /** Sort key inside the collection, dense from `0`. See {@link MovieCollectionsNamespace.reorder}. */
326
+ readonly position: number;
327
+ }
328
+ /** Arguments for {@link MovieCollectionsNamespace.create}. */
329
+ export interface CreateMovieCollectionInput {
330
+ /**
331
+ * Required. It is the ONLY field create reads: `before_create` overwrites
332
+ * `user`, forces `kind` to `"manual"` and computes `position` as
333
+ * `max(position) + 1`, so passing a kind or a position is silently ignored
334
+ * rather than rejected. There is no way to mint a second system collection.
335
+ */
336
+ readonly name: string;
337
+ }
338
+ /** Arguments for {@link MovieCollectionsNamespace.update}. */
339
+ export interface UpdateMovieCollectionInput {
340
+ readonly name?: string;
341
+ /**
342
+ * Sidebar order. Nothing normalises it: two collections can hold the same
343
+ * position and the server will not complain, so the client owns keeping the
344
+ * sequence sane.
345
+ */
346
+ readonly position?: number;
347
+ }
348
+ /**
349
+ * Filters for {@link MovieCollectionsNamespace.list}.
350
+ *
351
+ * Allowlist: `id`, `name`, `kind`, `created_at`, `updated_at`. Anything else is
352
+ * `400 "Unknown search filter: ..."`.
353
+ */
354
+ export interface ListMovieCollectionsParams extends PageParams {
355
+ /** Exact match, or `IN (...)` when given an array. */
356
+ readonly id?: Id | readonly Id[];
357
+ /**
358
+ * Partial, accent-folded, case-insensitive match - the `LIKE` a search box
359
+ * wants. NOT equality: `"fav"` matches `"Favoritos"`.
360
+ */
361
+ readonly name?: string;
362
+ /** Exact match. `"favorites"` finds the one system row. */
363
+ readonly kind?: MovieCollectionKind;
364
+ }
365
+ /** Arguments for {@link MovieCollectionItemsNamespace.create}. */
366
+ export interface CreateMovieCollectionItemInput {
367
+ readonly movie_collection_id: Id;
368
+ readonly movie_type: MovieType;
369
+ /** The addon's id for the title. Required, and part of the uniqueness key. */
370
+ readonly movie_id: string;
371
+ readonly name?: string | null;
372
+ readonly poster?: string | null;
373
+ readonly background?: string | null;
374
+ readonly release_info?: string | null;
375
+ }
376
+ /**
377
+ * Filters for {@link MovieCollectionItemsNamespace.list}.
378
+ *
379
+ * Allowlist: `id`, `movie_collection_id`, `movie_type`, `movie_id`,
380
+ * `position`, `created_at`, `updated_at`.
381
+ */
382
+ export interface ListMovieCollectionItemsParams extends PageParams {
383
+ /** Exact match, or `IN (...)` when given an array. */
384
+ readonly id?: Id | readonly Id[];
385
+ /**
386
+ * Exact match, or `IN (...)` for several collections at once. Almost always
387
+ * what you want: the bare index returns the items of EVERY collection the
388
+ * caller owns, interleaved.
389
+ */
390
+ readonly collectionId?: Id | readonly Id[];
391
+ /** Exact match. */
392
+ readonly movieType?: MovieType;
393
+ /** Exact match on the addon's title id. */
394
+ readonly movieId?: string;
395
+ /** Exact match on the sort key. */
396
+ readonly position?: number;
397
+ }
398
+ /**
399
+ * Fraction of the runtime that counts as watched when the server derives
400
+ * `finished` itself. Mirrors `MovieWatchProgress::FINISHED_THRESHOLD`.
401
+ */
402
+ export declare const MOVIE_WATCH_FINISHED_THRESHOLD = 0.95;
403
+ /**
404
+ * Rows `POST /movie_watch_progresses/bulk` will accept in one call. Mirrors
405
+ * `MovieWatchProgressesController::BULK_LIMIT`.
406
+ *
407
+ * The server does `Array(params[:items]).first(200)`: entries past the limit
408
+ * are dropped in SILENCE and the call still answers `200` with the 200 rows it
409
+ * did save, so a client marking a 300-episode series watched would believe it
410
+ * succeeded. {@link MovieWatchProgressesNamespace.saveMany} raises instead.
411
+ */
412
+ export declare const MOVIE_WATCH_BULK_LIMIT = 200;
413
+ /**
414
+ * Rows `GET /movie_watch_progresses` returns, at most. Hard-coded `limit(500)`
415
+ * in the controller, with no paging and no way to reach row 501 - see
416
+ * {@link MovieWatchProgressesNamespace.list}.
417
+ */
418
+ export declare const MOVIE_WATCH_LIST_LIMIT = 500;
419
+ /** One title-or-episode the user has started, and how far in they got. */
420
+ export interface MovieWatchProgress extends BaseRecord {
421
+ readonly user_id: Id;
422
+ readonly movie_type: MovieType;
423
+ /** The addon's id for the TITLE. A series shares it across every episode. */
424
+ readonly movie_id: string;
425
+ /**
426
+ * The addon's id for the specific playable. For a film this is usually the
427
+ * same string as `movie_id`; for a series it is the episode.
428
+ *
429
+ * `(user_id, movie_id, video_id)` is the row's identity and carries a unique
430
+ * index.
431
+ */
432
+ readonly video_id: string;
433
+ readonly season: number | null;
434
+ readonly episode: number | null;
435
+ readonly name: string | null;
436
+ readonly episode_title: string | null;
437
+ readonly poster: string | null;
438
+ /** Seconds into the playable. A float, `NOT NULL DEFAULT 0.0`. */
439
+ readonly position: number;
440
+ /** Runtime in seconds, as the player measured it. `0` when unknown. */
441
+ readonly duration: number;
442
+ /** See {@link MovieWatchProgressInput.finished} for how this gets its value. */
443
+ readonly finished: boolean;
444
+ /** What "Continuar a ver" sorts on, descending. */
445
+ readonly last_watched_at: Timestamp;
446
+ }
447
+ /**
448
+ * One row to upsert, through {@link MovieWatchProgressesNamespace.save} or
449
+ * {@link MovieWatchProgressesNamespace.saveMany}.
450
+ *
451
+ * `movie_id`, `video_id` and `movie_type` are checked up front by the
452
+ * controller and a missing one is
453
+ * `400 "movie_id, video_id, movie_type are required"`. Everything else is
454
+ * optional, but read the notes on `finished` and `last_watched_at` before
455
+ * leaving them out: both are cases where omitting the field does something
456
+ * other than "leave it as it was".
457
+ */
458
+ export interface MovieWatchProgressInput {
459
+ readonly movie_type: MovieType;
460
+ /** Identity, with `video_id`. Changing `movie_type` does NOT make a new row. */
461
+ readonly movie_id: string;
462
+ /** Identity, with `movie_id`. */
463
+ readonly video_id: string;
464
+ readonly season?: number | null;
465
+ readonly episode?: number | null;
466
+ readonly name?: string | null;
467
+ readonly episode_title?: string | null;
468
+ readonly poster?: string | null;
469
+ /** Seconds into the playable. Negative is `400 "Position must be greater than or equal to 0"`. */
470
+ readonly position?: number;
471
+ /** Runtime in seconds. Negative is a `400` the same way. */
472
+ readonly duration?: number;
473
+ /**
474
+ * Three states, not two.
475
+ *
476
+ * - **omitted** - the server derives it:
477
+ * `finished = position >= duration * 0.95`, and if `duration <= 0` it
478
+ * leaves the stored flag ALONE. This is what a playback tick should send.
479
+ * - **`true` / `false`** - the user said so. `upsert_for` sets the model's
480
+ * `finished_given` flag, which makes `set_finished` return before its
481
+ * `duration <= 0` guard, so the value is written as given.
482
+ * - **`null`** - deleted by `progress_params` before it reaches the model
483
+ * (`permitted.delete(:finished) if permitted[:finished].nil?`) and
484
+ * therefore identical to omitting it.
485
+ *
486
+ * That last branch is the bug that was fixed here. "Marcar como nao visto"
487
+ * sends `position: 0, duration: 0`, which used to hit the `duration <= 0`
488
+ * guard and leave `finished` true forever. Sending an explicit `false` is
489
+ * what makes it stick - sending `null`, or leaving the key out, still does
490
+ * nothing at all. {@link MovieWatchProgressesNamespace.setWatched} spells it
491
+ * out so you cannot get this wrong by accident.
492
+ *
493
+ * `finished_given` is a plain `attr_accessor`, not a column, so it only
494
+ * pins the value for THAT save. The next tick that omits `finished` goes
495
+ * back to deriving it from the position.
496
+ */
497
+ readonly finished?: boolean;
498
+ /**
499
+ * When the user last watched, ISO-8601.
500
+ *
501
+ * SEND IT ON EVERY CALL. The model only defaults it with
502
+ * `self.last_watched_at ||= Time.current`, which is a no-op on an existing
503
+ * row - so an upsert that omits it keeps whatever timestamp was there when
504
+ * the row was first created. The Continue Watching list is
505
+ * `order(last_watched_at: :desc)`, so omitting this pins the title where it
506
+ * first appeared and it never moves back to the front. `new Date().toISOString()`
507
+ * at the call site is the whole fix.
508
+ */
509
+ readonly last_watched_at?: Timestamp;
510
+ }
511
+ /**
512
+ * Whether a collection is the server-managed favourites row.
513
+ *
514
+ * Prefer this over `collection.kind === "favorites"`: the server's own test is
515
+ * `kind != "manual"`, so a future system kind reads as system there and would
516
+ * read as manual in a hand-written equality check. The blueprint already
517
+ * computes it; this just keeps the test in one place.
518
+ */
519
+ export declare function isSystemMovieCollection(collection: Pick<MovieCollection, "kind">): boolean;
520
+ /**
521
+ * The same arithmetic `MovieWatchProgress#set_finished` uses, for a client that
522
+ * wants to render a "watched" tick before the round trip lands.
523
+ *
524
+ * Returns `null` - not `false` - when `duration` is zero or negative, because
525
+ * that is precisely the case where the server declines to decide and leaves the
526
+ * stored flag untouched. Treating that as `false` is how an optimistic UI ends
527
+ * up un-ticking something the server still considers watched.
528
+ */
529
+ export declare function movieWatchFinished(position: number, duration: number): boolean | null;
530
+ /**
531
+ * The `movies.addons.groups` namespace: named folders of addons, which double
532
+ * as sharing units.
533
+ *
534
+ * There is no `show` route. Read one out of {@link list}.
535
+ */
536
+ export declare class MovieAddonGroupsNamespace extends Resource {
537
+ /**
538
+ * `GET /movie_addon_groups` - the caller's own groups.
539
+ *
540
+ * `viewable_by` is `where(user: user)` with no grant clause, so a group
541
+ * somebody shared with you never appears here even though its addons do.
542
+ * The shared addons arrive from {@link MovieAddonsNamespace.list} carrying a
543
+ * `movie_addon_group_id` you cannot resolve; render them under a single
544
+ * "shared with me" heading rather than trying to look the group up.
545
+ *
546
+ * The relation has NO order of its own, so pass `order` (typically
547
+ * `"name:asc"` or `"created_at:desc"`) - paging an unordered relation can
548
+ * repeat and drop rows.
549
+ *
550
+ * @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
551
+ * @throws {OmsApiError} 400 `"Unknown search filter: ..."` for any filter
552
+ * beyond `id`, `created_at` and `updated_at`.
553
+ */
554
+ list(params?: ListMovieAddonGroupsParams, options?: RequestOptions): Promise<Paginated<MovieAddonGroup>>;
555
+ /**
556
+ * `POST /movie_addon_groups` - creates a group owned by the caller. `201`.
557
+ *
558
+ * `name` is the only writable field; the owner is forced to `Current.user` in
559
+ * a `before_validation`, so there is no way to create one for somebody else.
560
+ *
561
+ * Names are NOT unique: two groups called "Filmes" are allowed and will look
562
+ * identical in a picker. De-duplicate client-side if that matters.
563
+ *
564
+ * @throws {OmsApiError} 400 `"Name can't be blank"` or
565
+ * `"Name is too long (maximum is 80 characters)"`.
566
+ */
567
+ create(input: CreateMovieAddonGroupInput | string, options?: RequestOptions): Promise<MovieAddonGroup>;
568
+ /**
569
+ * `PATCH /movie_addon_groups/:id` - renames a group. `200`.
570
+ *
571
+ * Unlike its sibling {@link MovieAddonsNamespace.update}, this one is a real
572
+ * partial update: `update_params :name` permits exactly one key and nothing
573
+ * is assigned behind your back.
574
+ *
575
+ * @throws {OmsApiError} 404 `"Resource not found"` for an id that is not
576
+ * yours - ownership is applied by the lookup scope, so somebody else's
577
+ * group is indistinguishable from a group that does not exist.
578
+ */
579
+ update(id: Id, name: string, options?: RequestOptions): Promise<MovieAddonGroup>;
580
+ /**
581
+ * `DELETE /movie_addon_groups/:id` - `204`, empty body.
582
+ *
583
+ * Two different cascades, and only one of them destroys anything:
584
+ *
585
+ * - the addons inside are `dependent: :nullify`, so they SURVIVE and become
586
+ * ungrouped;
587
+ * - the grants on the group are `dependent: :destroy`, so every share made
588
+ * through this group is revoked. People who could see those addons stop
589
+ * seeing them, with no notification. Direct grants on the individual
590
+ * addons are untouched.
591
+ *
592
+ * @throws {OmsApiError} 404 for an id that is not yours.
593
+ */
594
+ delete(id: Id, options?: RequestOptions): Promise<void>;
595
+ }
596
+ /**
597
+ * The `movies.addons.grants` namespace: sharing an addon, or a whole group,
598
+ * with one other user.
599
+ *
600
+ * Grants are create-and-delete only; there is no update route and no `show`.
601
+ * To change who can see what, delete the grant and make a new one.
602
+ */
603
+ export declare class MovieAddonGrantsNamespace extends Resource {
604
+ /**
605
+ * `GET /movie_addon_grants` - every grant the caller made OR received.
606
+ *
607
+ * Both directions come back in one undifferentiated list
608
+ * (`where(grantor: user).or(where(grantee: user))`) and the index accepts no
609
+ * filter to separate them, so split on `grantor_id === myUserId` yourself.
610
+ *
611
+ * The relation has no order of its own; pass `order: "created_at:desc"`.
612
+ *
613
+ * @throws {OmsApiError} 400 `"Unknown search filter: ..."` for anything
614
+ * beyond `id`, `created_at` and `updated_at`. See
615
+ * {@link ListMovieAddonGrantsParams}.
616
+ */
617
+ list(params?: ListMovieAddonGrantsParams, options?: RequestOptions): Promise<Paginated<MovieAddonGrant>>;
618
+ /**
619
+ * `POST /movie_addon_grants` - shares one addon, or one group, with one user.
620
+ * `201`.
621
+ *
622
+ * The grantor is always the caller: it is forced in a `before_validation` and
623
+ * re-checked on save, so there is no way to make a grant in somebody else's
624
+ * name.
625
+ *
626
+ * ## Failure modes, and which status each one is
627
+ *
628
+ * - **both targets, or neither** - `400 "Grant must target one addon or one
629
+ * group"`. Caught here before the request goes out.
630
+ * - **a target you do not own, or granting to YOURSELF** - `401 "You are not
631
+ * authorized to create this resource"`. Not a `400` and not a `403`:
632
+ * `creatable_by?` runs inside `CrudActions#create`, so a business-rule
633
+ * violation comes back wearing an authentication status. A generic error
634
+ * handler that logs the user out on `401` will do exactly that here.
635
+ * - **granting the same target to the same person twice** - this is the one
636
+ * to be careful with. There is a partial unique index on
637
+ * `(movie_addon_id, grantee_id)` and another on
638
+ * `(movie_addon_group_id, grantee_id)`, and there is NO matching
639
+ * `validates :uniqueness` on the model, so the duplicate is not a tidy
640
+ * `400`: it raises `ActiveRecord::RecordNotUnique` out of `save`, lands in
641
+ * the global rescue, answers **500** and fires a Discord error alert. List
642
+ * the existing grants and check before you create, and do not put this call
643
+ * behind a blind retry.
644
+ *
645
+ * Retries are off by default for a POST anyway (the transport only replays
646
+ * safe methods unless you opt in); do not opt in here.
647
+ *
648
+ * @throws {OmsError} `invalid_request` when the target count is not exactly
649
+ * one, or `grantee_id` is blank.
650
+ */
651
+ create(input: CreateMovieAddonGrantInput, options?: RequestOptions): Promise<MovieAddonGrant>;
652
+ /**
653
+ * `DELETE /movie_addon_grants/:id` - revokes a share. `204`, empty body.
654
+ *
655
+ * Only the GRANTOR may revoke: `destroyable_by?` is `grantor == user`. The
656
+ * grantee can see the grant in {@link list} but deleting it is
657
+ * `401 "You are not authorized to destroy this resource"` - there is no
658
+ * "leave this share" for the receiving side.
659
+ *
660
+ * @throws {OmsApiError} 404 for a grant that is neither yours nor shared
661
+ * with you, 401 when you are the grantee rather than the grantor.
662
+ */
663
+ delete(id: Id, options?: RequestOptions): Promise<void>;
664
+ }
665
+ /**
666
+ * The `movies.addons` namespace: installed Stremio addons, plus the groups and
667
+ * grants that share them.
668
+ *
669
+ * There is no `show` route on `/movie_addons`; {@link list} is how you read
670
+ * one.
671
+ */
672
+ export declare class MovieAddonsNamespace extends Resource {
673
+ /** Named folders of addons, which are also the unit of sharing. */
674
+ readonly groups: MovieAddonGroupsNamespace;
675
+ /** Shares of an addon or a group with another user. */
676
+ readonly grants: MovieAddonGrantsNamespace;
677
+ constructor(http: ApiClient);
678
+ /**
679
+ * `GET /movie_addons` - the caller's addons AND every addon shared with them.
680
+ *
681
+ * `viewable_by` is a three-way `OR`: rows you own, rows granted to you
682
+ * directly, and rows whose group was granted to you. That last arm is why a
683
+ * grant on a group covers addons added to it later. The result is
684
+ * `.distinct`, so an addon shared both ways still appears once.
685
+ *
686
+ * Read {@link MovieAddon.shared} to tell the two kinds apart. Everything is
687
+ * read-only for a shared row.
688
+ *
689
+ * The relation has NO default order. Always pass `order` - `"created_at:desc"`
690
+ * matches the web app's "most recently installed first" - because paging an
691
+ * unordered Postgres relation can repeat and drop rows between pages.
692
+ *
693
+ * **Array filters are silently ignored on this index.** `search_params` here
694
+ * declares only scalars, so `id` given as an array is dropped by `permit`
695
+ * without a `400` and you get the UNFILTERED list back. The SDK only accepts
696
+ * a scalar for that reason. Unknown filter KEYS do fail closed with a `400`.
697
+ *
698
+ * This index emits an `ETag` and can answer `304`. Do not hand-write an
699
+ * `If-None-Match` header: the transport treats a bare `304` as a failure.
700
+ *
701
+ * @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
702
+ */
703
+ list(params?: ListMovieAddonsParams, options?: RequestOptions): Promise<Paginated<MovieAddon>>;
704
+ /**
705
+ * `POST /movie_addons` - installs an addon, or re-installs one you already
706
+ * have. **`201` either way.**
707
+ *
708
+ * The controller does `find_or_initialize_by(user: Current.user,
709
+ * manifest_url: ...)` before the generic create runs, so posting a manifest
710
+ * URL you already installed UPDATES that row - refreshing `manifest_json` and
711
+ * reassigning `movie_addon_group_id` - instead of colliding with the
712
+ * `(user_id, manifest_url)` unique index. The status stays `201` and the `id`
713
+ * comes back unchanged, so `201` here does not mean "new row"; compare
714
+ * `created_at` if you need to know.
715
+ *
716
+ * The practical consequence is the good one: reinstalling is idempotent and
717
+ * the app can re-post its whole addon list on boot. The trap is the other
718
+ * side of it - re-posting with `movie_addon_group_id` omitted leaves the
719
+ * group as it was (the key is only assigned when present in `params.permit`),
720
+ * while re-posting it as `null` clears the group.
721
+ *
722
+ * `manifest_json` is stored with `to_unsafe_h`: no key is validated, no key
723
+ * is stripped, and whatever you send is what everyone the addon is shared
724
+ * with will later render. Fetch the manifest yourself and do not forward one
725
+ * a third party handed you unchecked.
726
+ *
727
+ * @throws {OmsApiError} 400 `"Manifest url must be a valid URL"`,
728
+ * `"Manifest json can't be blank"` (an empty object counts as blank), or
729
+ * `"Movie addon group must belong to addon owner"`.
730
+ */
731
+ create(input: CreateMovieAddonInput, options?: RequestOptions): Promise<MovieAddon>;
732
+ /**
733
+ * `PATCH /movie_addons/:id` - `200`.
734
+ *
735
+ * **This is not a partial update, whatever the verb says.**
736
+ * `movie_addon_params` is shared between create and update and finishes with
737
+ * an unconditional `permitted[:manifest_json] = params[:manifest_json]`, so
738
+ * the key is always assigned - as `nil` when you did not send one. `nil` then
739
+ * fails `validates :manifest_json, presence: true` and the whole call is
740
+ * `400 "Manifest json can't be blank"`. Nothing is written; the row is fine.
741
+ * It simply cannot be patched without resending the manifest.
742
+ *
743
+ * That is a live divergence: the web frontend's
744
+ * `MovieAddonsService.update(id, { movie_addon_group_id })` sends the group
745
+ * alone and therefore 400s. {@link UpdateMovieAddonInput.manifest_json} is
746
+ * required here so the same mistake is a compile error, and
747
+ * {@link moveToGroup} carries the manifest across for you.
748
+ *
749
+ * Only the owner may update: a shared row is `401`.
750
+ *
751
+ * @throws {OmsError} `invalid_request` when `manifest_json` is missing or empty.
752
+ * @throws {OmsApiError} 404 for an id you cannot see, 401 for one you can see
753
+ * but do not own.
754
+ */
755
+ update(id: Id, input: UpdateMovieAddonInput, options?: RequestOptions): Promise<MovieAddon>;
756
+ /**
757
+ * Files an addon under a group, or un-groups it with `null`.
758
+ *
759
+ * Sugar over {@link update} that exists purely because the endpoint demands
760
+ * the manifest back on every patch. Pass the {@link MovieAddon} you already
761
+ * hold and its `manifest_json` is resent unchanged; there is no extra request
762
+ * and no fetch of the manifest.
763
+ *
764
+ * Refuses a shared addon before the round trip: the server would answer
765
+ * `401`, and the message here says why.
766
+ *
767
+ * @throws {OmsError} `invalid_request` for an addon whose `shared` flag is set.
768
+ */
769
+ moveToGroup(addon: MovieAddon, groupId: Id | null, options?: RequestOptions): Promise<MovieAddon>;
770
+ /**
771
+ * `DELETE /movie_addons/:id` - uninstalls. `204`, empty body.
772
+ *
773
+ * Owner only; uninstalling an addon somebody shared with you is `401`, and
774
+ * the way to lose one of those is for the grantor to revoke the grant.
775
+ *
776
+ * Every grant ON this addon is `dependent: :destroy`, so deleting it revokes
777
+ * the shares along with it. Grants that reached people through its GROUP are
778
+ * untouched - they belong to the group, which still exists.
779
+ *
780
+ * @throws {OmsApiError} 404 for an id you cannot see, 401 for a shared one.
781
+ */
782
+ delete(id: Id, options?: RequestOptions): Promise<void>;
783
+ }
784
+ /**
785
+ * The `movies.collections.items` namespace: the titles filed into a collection.
786
+ *
787
+ * Index and create and delete; there is no `show` and no `update`. To change a
788
+ * title's stored metadata, {@link create} it again - it upserts.
789
+ */
790
+ export declare class MovieCollectionItemsNamespace extends Resource {
791
+ /**
792
+ * `GET /movie_collection_items` - items across the caller's collections.
793
+ *
794
+ * **Pass `collectionId` unless you really mean everything.** With no filter
795
+ * this returns the items of EVERY collection the caller owns, and because
796
+ * `MovieCollectionItem` carries `default_scope { order(position: :asc) }`
797
+ * they come back interleaved by position rather than grouped by collection -
798
+ * position 0 of each list, then position 1 of each, and so on. Grouping that
799
+ * back together client-side works but reads like a bug when you first see it.
800
+ *
801
+ * `collectionId` accepts an array, which becomes `IN (...)`: one request for
802
+ * the three lists a screen shows.
803
+ *
804
+ * The default order is `position:asc` and it is the useful one, so leave
805
+ * `order` alone unless you want something else. Note that passing `order`
806
+ * REPLACES the default (`QueryModifier` uses `reorder`), it does not add to
807
+ * it, so `order: "created_at:desc"` loses the position ordering entirely.
808
+ *
809
+ * @throws {OmsApiError} 400 `"Unknown search filter: ..."` outside the
810
+ * allowlist in {@link ListMovieCollectionItemsParams}.
811
+ */
812
+ list(params?: ListMovieCollectionItemsParams, options?: RequestOptions): Promise<Paginated<MovieCollectionItem>>;
813
+ /**
814
+ * `POST /movie_collection_items` - adds a title to a collection, or refreshes
815
+ * the one already there. **`201` either way.**
816
+ *
817
+ * The controller does `find_or_initialize_by(movie_collection_id, movie_type,
818
+ * movie_id)` first, so adding the same title twice is a no-op-with-an-update
819
+ * rather than a `400` off the unique index. This is deliberate: the heart
820
+ * button and the "add to list" dialog both fire blind, holding only the
821
+ * collection they already loaded. The upshot is that this call is safe to
822
+ * repeat and safe to fire optimistically.
823
+ *
824
+ * Two consequences worth knowing:
825
+ *
826
+ * - `position` is only computed for a NEW row (`max(position) + 1`, starting
827
+ * at `0`). Re-adding an existing title keeps its place in the list rather
828
+ * than moving it to the end.
829
+ * - the denormalised metadata (`name`, `poster`, `background`,
830
+ * `release_info`) IS overwritten every time, so re-posting is how you
831
+ * refresh a poster that the addon has since changed. Sending `null` for one
832
+ * clears it; omitting the key leaves the stored value alone.
833
+ *
834
+ * A `movie_collection_id` that does not exist, or belongs to somebody else,
835
+ * is `401 "You are not authorized to create this resource"` and NOT a `404`:
836
+ * `creatable_by?` reads `movie_collection&.user == user`, and a missing
837
+ * collection makes that `nil == user`, which is false. Do not read that `401`
838
+ * as "the session expired".
839
+ *
840
+ * Adding to the favourites collection is allowed - the system flag blocks
841
+ * renaming, reordering and deleting the COLLECTION, not writing items into
842
+ * it. That is how the heart button works.
843
+ *
844
+ * @throws {OmsError} `invalid_request` when the collection id, type or movie
845
+ * id is blank.
846
+ * @throws {OmsApiError} 400 `"Movie type can't be blank"` /
847
+ * `"Movie can't be blank"`, 401 for a collection that is not yours.
848
+ */
849
+ create(input: CreateMovieCollectionItemInput, options?: RequestOptions): Promise<MovieCollectionItem>;
850
+ /**
851
+ * `DELETE /movie_collection_items/:id` - `204`, empty body.
852
+ *
853
+ * The id is the ITEM's primary key, not the `movie_id` the addon uses. If
854
+ * all you hold is a title, {@link list} it with `collectionId` and `movieId`
855
+ * first, or keep the item rows the collection screen already loaded.
856
+ *
857
+ * Removing leaves a gap in `position`: nothing renumbers the survivors, and
858
+ * the next {@link create} takes `max + 1`, so positions drift sparse over
859
+ * time. Only {@link MovieCollectionsNamespace.reorder} makes them dense
860
+ * again. Nothing depends on them being dense.
861
+ *
862
+ * @throws {OmsApiError} 404 for an item outside your collections.
863
+ */
864
+ delete(id: Id, options?: RequestOptions): Promise<void>;
865
+ }
866
+ /**
867
+ * The `movies.collections` namespace: the favourites row plus whatever lists
868
+ * the user built by hand.
869
+ */
870
+ export declare class MovieCollectionsNamespace extends Resource {
871
+ /** The titles inside a collection. */
872
+ readonly items: MovieCollectionItemsNamespace;
873
+ constructor(http: ApiClient);
874
+ /**
875
+ * `GET /movie_collections` - the caller's collections, each with its
876
+ * `items_count`.
877
+ *
878
+ * **This call has a side effect, and it is the only one that does.**
879
+ * `listing_scope` runs `MovieCollection.favorites_for(Current.user)` before
880
+ * anything else, which `find_or_create_by!`s the "Favoritos" row (kind
881
+ * `favorites`, position `-1`). The index is the only place that knows the
882
+ * user has opened the movies app, so it is where the row gets minted. Two
883
+ * consequences: a brand new account's first listing WRITES to the database,
884
+ * and there is no other way to make favourites exist - a client that goes
885
+ * straight to the heart button without ever listing has no collection to put
886
+ * the title in. List first.
887
+ *
888
+ * A concurrent second tab racing the same first listing is handled: the
889
+ * partial unique index raises, the model rescues `RecordNotUnique` and reads
890
+ * the winner's row back.
891
+ *
892
+ * `items_count` is exact and free - the controller preloads
893
+ * `:movie_collection_items` so the blueprint counts a loaded array instead of
894
+ * firing a `COUNT` per row. {@link get} does not preload, so it costs one
895
+ * `COUNT` there. Neither is a snapshot you can trust after a write.
896
+ *
897
+ * No default order. Pass `order: "position:asc"` to get the sidebar's own
898
+ * order, which puts favourites first by virtue of its `-1`.
899
+ *
900
+ * @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
901
+ */
902
+ list(params?: ListMovieCollectionsParams, options?: RequestOptions): Promise<Paginated<MovieCollection>>;
903
+ /**
904
+ * Reads the caller's favourites collection, creating it if this is the first
905
+ * time they have opened the app.
906
+ *
907
+ * A one-line convenience over {@link list} that exists because "get me the
908
+ * heart list" is the single most common reason to call the index, and
909
+ * because doing it by hand invites filtering on `kind` client-side after a
910
+ * listing that may have been paged.
911
+ *
912
+ * Resolves to `null` only if the server somehow answered without the row,
913
+ * which should not happen - the listing mints it.
914
+ */
915
+ favorites(options?: RequestOptions): Promise<MovieCollection | null>;
916
+ /**
917
+ * `GET /movie_collections/:id` - one collection.
918
+ *
919
+ * The only `show` route in this whole namespace. It renders the `:extended`
920
+ * view, which for these blueprints is byte-identical to the default view the
921
+ * index returns: `ApplicationBlueprint` declares `view :extended do end`, and
922
+ * a Blueprinter view INHERITS the base fields and adds nothing here. So
923
+ * `show` is not a richer payload, only a single-row one.
924
+ *
925
+ * Unlike {@link list} it does NOT mint the favourites row.
926
+ *
927
+ * @throws {OmsApiError} 404 `"Resource not found"` for a collection that is
928
+ * not yours. Ownership is the lookup scope, so somebody else's collection
929
+ * and a non-existent one are indistinguishable.
930
+ */
931
+ get(id: Id, options?: RequestOptions): Promise<MovieCollection>;
932
+ /**
933
+ * `POST /movie_collections` - a new manual list. `201`.
934
+ *
935
+ * `name` is the only field that survives: `before_create` overwrites `user`,
936
+ * pins `kind` to `"manual"` and sets `position` to `max(position) + 1`.
937
+ * Passing `kind: "favorites"` does not fail, it is just ignored, which is the
938
+ * point - there is exactly one system collection and only the listing may
939
+ * mint it.
940
+ *
941
+ * Names are not unique.
942
+ *
943
+ * @throws {OmsApiError} 400 `"Name can't be blank"`.
944
+ */
945
+ create(input: CreateMovieCollectionInput | string, options?: RequestOptions): Promise<MovieCollection>;
946
+ /**
947
+ * `PATCH /movie_collections/:id` - renames or repositions. `200`.
948
+ *
949
+ * A real partial update: `update_params :name, :position` permits those two
950
+ * and nothing else, and an omitted key is left alone.
951
+ *
952
+ * Refused for the favourites row with
953
+ * `401 "You are not authorized to update this resource"` - the GENERIC
954
+ * message, because `updatable_by?` already returns false for a system
955
+ * collection and the friendlier "The favourites collection cannot be renamed,
956
+ * reordered or deleted" in `before_update` is never reached. Only
957
+ * {@link reorder} produces that sentence. Test with
958
+ * {@link isSystemMovieCollection} and hide the control instead.
959
+ *
960
+ * @throws {OmsApiError} 404 for a collection that is not yours, 401 for the
961
+ * favourites row.
962
+ */
963
+ update(id: Id, input: UpdateMovieCollectionInput, options?: RequestOptions): Promise<MovieCollection>;
964
+ /**
965
+ * `DELETE /movie_collections/:id` - `204`, empty body.
966
+ *
967
+ * Cascades: `movie_collection_items` is `dependent: :destroy`, so every title
968
+ * in the list goes with it. Nothing is recoverable and nothing is asked.
969
+ *
970
+ * Refused for the favourites row with the same generic `401` as
971
+ * {@link update}, for the same reason.
972
+ */
973
+ delete(id: Id, options?: RequestOptions): Promise<void>;
974
+ /**
975
+ * `POST /movie_collections/:id/reorder` - rewrites the order of the items
976
+ * inside a collection. Answers `200` with the COLLECTION, not the items.
977
+ *
978
+ * Send the full ordered list of item ids. The server keeps a stale client
979
+ * from losing rows: ids it recognises are laid out first in the order given,
980
+ * then every item you did NOT mention is appended in its existing relative
981
+ * order, and the whole sequence is renumbered densely from `0`. So an item
982
+ * added by another tab between your read and your write sinks to the bottom
983
+ * instead of vanishing. Ids that are not in this collection are ignored, not
984
+ * rejected.
985
+ *
986
+ * Positions are written with `update_column` inside one transaction, which
987
+ * skips validations and callbacks and - the part that catches people -
988
+ * does NOT touch each item's `updated_at`. A client that syncs on
989
+ * `updated_at` will not see a reorder. Refetch by position.
990
+ *
991
+ * The whole rewrite is one transaction, and items already at the right
992
+ * position are skipped, so a reorder that changes nothing costs no writes.
993
+ *
994
+ * Refused for the favourites row with
995
+ * `401 "The favourites collection cannot be renamed, reordered or deleted"` -
996
+ * this is the one action that produces that message rather than the generic
997
+ * one, because `reorder` calls `refuse_if_system!` itself before any
998
+ * authorisation runs.
999
+ *
1000
+ * @throws {OmsApiError} 404 for a collection that is not yours, 401 for the
1001
+ * favourites row.
1002
+ */
1003
+ reorder(id: Id, itemIds: readonly Id[], options?: RequestOptions): Promise<MovieCollection>;
1004
+ }
1005
+ /**
1006
+ * The `movies.watchProgress` namespace: how far into each title the user got,
1007
+ * and what "Continuar a ver" is built from.
1008
+ *
1009
+ * This controller is the odd one out. It overrides `index`, `create` and
1010
+ * `destroy` instead of inheriting `CrudActions`, so none of the list DSL
1011
+ * applies, `create` answers `200`, and there is no `show` and no `update`.
1012
+ * Everything is an upsert keyed on `(user, movie_id, video_id)`.
1013
+ */
1014
+ export declare class MovieWatchProgressesNamespace extends Resource {
1015
+ /**
1016
+ * `GET /movie_watch_progresses` - the caller's rows, newest first.
1017
+ *
1018
+ * Not paginated, and not filterable. The controller ignores the query string
1019
+ * entirely and runs a fixed
1020
+ * `order(last_watched_at: :desc).limit(500)`, so:
1021
+ *
1022
+ * - there is NO way to reach row 501. A user with more history than that
1023
+ * simply cannot read the tail through this API;
1024
+ * - `search[...]` / `exact_search[...]` / `modifiers[...]` are not rejected,
1025
+ * they are silently ignored - this index never touches the code that
1026
+ * raises `400 "Unknown search filter"`. A client that thinks it asked for
1027
+ * one title gets all 500 rows and, if it trusts the filter, the wrong
1028
+ * answer. Filter client-side; that is why this method takes no params;
1029
+ * - there is no `ETag` either, because it does not go through
1030
+ * `resources_stale?`.
1031
+ *
1032
+ * Finished rows are included. Build "Continuar a ver" by dropping
1033
+ * `finished === true` yourself, and remember an episode can be finished while
1034
+ * its series is not.
1035
+ *
1036
+ * @throws {OmsApiError} 401 for an anonymous caller, 403 for an OAuth token.
1037
+ */
1038
+ list(options?: RequestOptions): Promise<MovieWatchProgress[]>;
1039
+ /**
1040
+ * `POST /movie_watch_progresses` - upserts ONE row. **`200`, not `201`.**
1041
+ *
1042
+ * This is the playback tick: call it while something is playing, throttled
1043
+ * by the player to whatever interval you like. `MovieWatchProgress.upsert_for`
1044
+ * finds by `(user, movie_id, video_id)` and updates in place, so it is
1045
+ * idempotent and safe to repeat - the only create in this file where opting
1046
+ * into `options.retry` is a good idea rather than a way to make duplicates.
1047
+ * (The transport does not replay non-safe methods unless you ask.)
1048
+ *
1049
+ * `movie_type` is NOT part of the key. Posting the same `(movie_id,
1050
+ * video_id)` with a different type rewrites the existing row's type rather
1051
+ * than creating a second one.
1052
+ *
1053
+ * Read {@link MovieWatchProgressInput.finished} and
1054
+ * {@link MovieWatchProgressInput.last_watched_at} before using this: a tick
1055
+ * that omits `last_watched_at` does not move the title up the list, and
1056
+ * `finished` has three states rather than two. A `finished` of `null` is
1057
+ * dropped by this method rather than sent, matching what the server would do
1058
+ * with it, so `finished` reaches the wire only as a real boolean.
1059
+ *
1060
+ * Use {@link saveMany} instead when you have more than a couple of rows -
1061
+ * see its docs for why one request is not just faster but differently shaped.
1062
+ *
1063
+ * @throws {OmsError} `invalid_request` when `movie_id`, `video_id` or
1064
+ * `movie_type` is blank.
1065
+ * @throws {OmsApiError} 400 `"movie_id, video_id, movie_type are required"`
1066
+ * from the server's own check, or a validation sentence such as
1067
+ * `"Position must be greater than or equal to 0"`.
1068
+ */
1069
+ save(input: MovieWatchProgressInput, options?: RequestOptions): Promise<MovieWatchProgress>;
1070
+ /**
1071
+ * `POST /movie_watch_progresses/bulk` - upserts up to
1072
+ * {@link MOVIE_WATCH_BULK_LIMIT} rows in ONE request and ONE transaction.
1073
+ * `200`, with the saved rows in the order sent.
1074
+ *
1075
+ * ## Why this endpoint exists, and when to reach for it
1076
+ *
1077
+ * "Marcar temporada como vista" is one row per episode. A 24-episode season
1078
+ * through {@link save} is 24 POSTs: 24 round trips, 24 Puma threads taken in
1079
+ * turn, 24 chances for one to fail and leave the season half-marked, and 24
1080
+ * requests against the caller's 600/min ceiling. The comment on
1081
+ * `BULK_LIMIT` says it outright - "Marking a whole season watched would
1082
+ * otherwise be one POST per episode". This collapses it into one.
1083
+ *
1084
+ * The transaction is the other half of the point, and it cuts both ways:
1085
+ *
1086
+ * - **use {@link saveMany}** for a statement about several rows at once that
1087
+ * must be all-or-nothing - mark a season watched or unwatched, restore a
1088
+ * device's offline queue, seed history on first sync. If any entry is
1089
+ * invalid the whole batch rolls back and answers `400`, so you never end up
1090
+ * with episodes 1-9 marked and 10-24 not.
1091
+ * - **use {@link save}** for the continuous playback tick. One row, and a
1092
+ * failure costs one tick that the next one will overwrite anyway. Batching
1093
+ * ticks would trade a lost second for a lost minute.
1094
+ *
1095
+ * ## The silent-truncation trap this method closes
1096
+ *
1097
+ * The server does `Array(params[:items]).first(200)`: entries past the limit
1098
+ * are dropped without a word and the response is a cheerful `200` listing the
1099
+ * 200 that were saved. A 300-episode batch would look like it worked. This
1100
+ * method raises before sending instead, so split the work yourself - and note
1101
+ * that separate batches are separate transactions, so a split is no longer
1102
+ * atomic end to end.
1103
+ *
1104
+ * Retrying the whole batch is safe: every entry is the same upsert
1105
+ * {@link save} performs.
1106
+ *
1107
+ * @throws {OmsError} `invalid_request` for an empty list, for more than
1108
+ * {@link MOVIE_WATCH_BULK_LIMIT} entries, or for an entry missing
1109
+ * `movie_id`, `video_id` or `movie_type`.
1110
+ * @throws {OmsApiError} 400 `"items is required"` for an empty list that got
1111
+ * through, or the first failing entry's validation sentence - and in that
1112
+ * case NOTHING was saved.
1113
+ */
1114
+ saveMany(inputs: readonly MovieWatchProgressInput[], options?: RequestOptions): Promise<MovieWatchProgress[]>;
1115
+ /**
1116
+ * Marks one playable watched or unwatched, explicitly.
1117
+ *
1118
+ * Sugar over {@link save} that exists because this is the exact call the
1119
+ * fixed bug was about. Marking something UNWATCHED sends `position: 0,
1120
+ * duration: 0`, and with `finished` absent the model's `set_finished` bails
1121
+ * on its `duration <= 0` guard and leaves the stored flag as it was - so the
1122
+ * tick never came off. Passing the boolean outright sets `finished_given`,
1123
+ * which makes the model skip that guard and write what you said.
1124
+ *
1125
+ * Defaults `position` and `duration` to `0` when you do not supply them,
1126
+ * which is right for "mark unwatched" and harmless for "mark watched"
1127
+ * precisely because the explicit flag stops the server deriving anything from
1128
+ * them. Supply the real numbers when you have them; the progress bar reads
1129
+ * them.
1130
+ *
1131
+ * `last_watched_at` is still yours to send, and still matters: marking an
1132
+ * episode watched without one leaves the series where it was in the list.
1133
+ */
1134
+ setWatched(input: Omit<MovieWatchProgressInput, "finished">, watched: boolean, options?: RequestOptions): Promise<MovieWatchProgress>;
1135
+ /**
1136
+ * `DELETE /movie_watch_progresses/:id` - forgets ONE playable. `204`, empty
1137
+ * body.
1138
+ *
1139
+ * The id is the progress row's primary key, which for a series is one
1140
+ * episode. To forget a whole title use {@link forgetMovie}.
1141
+ *
1142
+ * @throws {OmsApiError} 404 `"Resource not found"` for a row that is not
1143
+ * yours.
1144
+ */
1145
+ delete(id: Id, options?: RequestOptions): Promise<void>;
1146
+ /**
1147
+ * `DELETE /movie_watch_progresses/for_movie?movie_id=...` - forgets EVERY row
1148
+ * for a title. `204`, empty body.
1149
+ *
1150
+ * This is the "remover de Continuar a ver" button. For a series it destroys
1151
+ * the progress of every episode, not just the one on screen; there is no
1152
+ * per-season form and no undo.
1153
+ *
1154
+ * Note the verb: despite the `?movie_id=` query string this is a **DELETE**,
1155
+ * not a read. It is a collection route, so the `movie_id` is the addon's id
1156
+ * for the title (an IMDb id, say), never a `movie_watch_progresses` primary
1157
+ * key.
1158
+ *
1159
+ * `204` even when nothing matched, so the answer does not tell you whether
1160
+ * anything was there.
1161
+ *
1162
+ * @throws {OmsError} `invalid_request` for a blank id.
1163
+ * @throws {OmsApiError} 400 `"movie_id is required"`.
1164
+ */
1165
+ forgetMovie(movieId: string, options?: RequestOptions): Promise<void>;
1166
+ }
1167
+ /**
1168
+ * The `movies` namespace, reachable as `oms.movies`.
1169
+ *
1170
+ * Everything under it needs a session or a personal token. An OAuth access
1171
+ * token cannot reach any of it: `enforce_oauth_scope!` denies by omission and
1172
+ * no movies controller declares an `oauth_scope`, so a third-party client gets
1173
+ * `403 {"error":"insufficient_scope", "message": "This endpoint is not
1174
+ * reachable with an OAuth access token..."}` - one of the few structured error
1175
+ * bodies the API emits. That is a deliberate gate, not an oversight to route
1176
+ * around.
1177
+ */
1178
+ export declare class MoviesNamespace extends Resource {
1179
+ /** Installed Stremio addons, plus `.groups` and `.grants` for sharing them. */
1180
+ readonly addons: MovieAddonsNamespace;
1181
+ /** Favourites and hand-made lists, plus `.items` for their contents. */
1182
+ readonly collections: MovieCollectionsNamespace;
1183
+ /** Playback position per title or episode; "Continuar a ver". */
1184
+ readonly watchProgress: MovieWatchProgressesNamespace;
1185
+ constructor(http: ApiClient);
1186
+ }