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