diff --git a/api/openapi.yaml b/api/openapi.yaml index 26c6aff..f92fcd2 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -15,6 +15,8 @@ tags: - name: library - name: catalog - name: search + - name: playback + - name: collection security: - bearerAuth: [] paths: @@ -769,6 +771,209 @@ paths: format: binary default: $ref: "#/components/responses/Problem" + /api/v1/stream/{id}: + get: + operationId: streamTrack + tags: [playback] + security: [] + summary: Аудио трека (поддерживает Range) + description: | + Ссылку целиком даёт `stream_url` трека (подпись пользователя и срок 12 ч); клиент может добавить `q`. + Файл отдаёт nginx через X-Accel-Redirect: оригинал из хранилища или транскод в Opus из кэша. + parameters: + - $ref: "#/components/parameters/EntityId" + - name: u + in: query + required: true + schema: + type: string + format: uuid + - name: exp + in: query + required: true + schema: + type: integer + format: int64 + - name: sig + in: query + required: true + schema: + type: string + - name: q + in: query + schema: + $ref: "#/components/schemas/StreamQuality" + responses: + "200": + description: Аудио + content: + audio/*: + schema: + type: string + format: binary + default: + $ref: "#/components/responses/Problem" + /api/v1/tracks/batch: + post: + operationId: getTracksBatch + tags: [catalog] + summary: Треки по списку id (восстановление очереди) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TrackIds" + responses: + "200": + description: Треки в порядке запроса; удалённые пропущены + content: + application/json: + schema: + $ref: "#/components/schemas/TrackList" + default: + $ref: "#/components/responses/Problem" + /api/v1/events: + post: + operationId: postEvents + tags: [playback] + summary: Пачка событий прослушивания (до 500) + description: Повторно присланные события (тот же id) не учитываются дважды. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventBatch" + responses: + "200": + description: Принято + content: + application/json: + schema: + $ref: "#/components/schemas/EventBatchResult" + default: + $ref: "#/components/responses/Problem" + /api/v1/me/playback-state: + get: + operationId: getPlaybackState + tags: [playback] + summary: Последнее состояние плеера (трек, позиция, очередь) + responses: + "200": + description: Состояние; пусто — плеер ещё не запускался + content: + application/json: + schema: + $ref: "#/components/schemas/PlaybackState" + default: + $ref: "#/components/responses/Problem" + put: + operationId: putPlaybackState + tags: [playback] + summary: Сохранить состояние плеера (last-write-wins) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PlaybackStateInput" + responses: + "200": + description: Сохранённое состояние + content: + application/json: + schema: + $ref: "#/components/schemas/PlaybackState" + default: + $ref: "#/components/responses/Problem" + /api/v1/me/history: + get: + operationId: getHistory + tags: [collection] + summary: История прослушиваний, новые сверху + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Cursor" + responses: + "200": + description: Страница истории + content: + application/json: + schema: + $ref: "#/components/schemas/HistoryPage" + default: + $ref: "#/components/responses/Problem" + delete: + operationId: clearHistory + tags: [collection] + summary: Очистить историю (события остаются для статистики) + responses: + "204": + description: Очищено + default: + $ref: "#/components/responses/Problem" + /api/v1/me/likes: + get: + operationId: getLikes + tags: [collection] + summary: Лайки, новые сверху + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Cursor" + responses: + "200": + description: Страница + content: + application/json: + schema: + $ref: "#/components/schemas/ReactedTrackPage" + default: + $ref: "#/components/responses/Problem" + /api/v1/me/dislikes: + get: + operationId: getDislikes + tags: [collection] + summary: Дизлайки, новые сверху + parameters: + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Cursor" + responses: + "200": + description: Страница + content: + application/json: + schema: + $ref: "#/components/schemas/ReactedTrackPage" + default: + $ref: "#/components/responses/Problem" + /api/v1/me/reactions/tracks/{id}: + parameters: + - $ref: "#/components/parameters/EntityId" + put: + operationId: setTrackReaction + tags: [collection] + summary: Лайк или дизлайк трека + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ReactionInput" + responses: + "204": + description: Сохранено + default: + $ref: "#/components/responses/Problem" + delete: + operationId: clearTrackReaction + tags: [collection] + summary: Снять реакцию + responses: + "204": + description: Снято + default: + $ref: "#/components/responses/Problem" components: parameters: SourceId: @@ -1466,7 +1671,7 @@ components: type: string Track: type: object - required: [id, title, duration_ms, artists, album, genres, disc_no, track_no, available] + required: [id, title, duration_ms, artists, album, genres, disc_no, track_no, available, stream_url, reaction] properties: id: type: string @@ -1494,6 +1699,14 @@ components: available: type: boolean description: Есть доступный файл (хранилище подключено, файл на месте) + stream_url: + type: string + nullable: true + description: Подписанная ссылка на аудио (12 ч); null — трек недоступен + reaction: + nullable: true + allOf: + - $ref: "#/components/schemas/Reaction" Artist: type: object required: [id, name, album_count, track_count] @@ -1599,6 +1812,216 @@ components: next_cursor: type: string nullable: true + StreamQuality: + type: string + enum: [auto, original, high, medium, low] + Reaction: + type: string + enum: [like, dislike] + TrackIds: + type: object + required: [ids] + properties: + ids: + type: array + maxItems: 500 + items: + type: string + format: uuid + TrackList: + type: object + required: [items] + properties: + items: + type: array + items: + $ref: "#/components/schemas/Track" + PlayEvent: + type: object + required: [id, type, track_id, occurred_at] + properties: + id: + type: string + format: uuid + description: UUIDv7 от клиента — ключ идемпотентности + type: + type: string + enum: [play_start, play_end, skip, seek, pause, resume, like, unlike, dislike, undislike] + track_id: + type: string + format: uuid + occurred_at: + type: string + format: date-time + position_ms: + type: integer + played_ms: + type: integer + from_ms: + type: integer + context_type: + type: string + enum: [wave, album, artist, search, collection, history, queue] + context_id: + type: string + maxLength: 100 + wave_session_id: + type: string + format: uuid + offline: + type: boolean + EventBatch: + type: object + required: [events] + properties: + events: + type: array + maxItems: 500 + items: + $ref: "#/components/schemas/PlayEvent" + EventBatchResult: + type: object + required: [accepted, duplicates, rejected] + properties: + accepted: + type: integer + duplicates: + type: integer + rejected: + type: integer + description: Слишком старые (> 30 дней) или с неизвестным треком + PlaybackContext: + type: object + properties: + type: + type: string + id: + type: string + label: + type: string + wave_session_id: + type: string + format: uuid + PlaybackStateInput: + type: object + required: [track_id, position_ms, queue, queue_index] + properties: + track_id: + type: string + format: uuid + nullable: true + position_ms: + type: integer + minimum: 0 + queue: + type: array + maxItems: 500 + items: + type: string + format: uuid + queue_index: + type: integer + minimum: 0 + context: + $ref: "#/components/schemas/PlaybackContext" + shuffle: + type: boolean + repeat_mode: + type: string + enum: ["off", one, all] + volume: + type: number + minimum: 0 + maximum: 1 + device_id: + type: string + maxLength: 100 + PlaybackState: + type: object + required: [track_id, position_ms, queue, queue_index, context, shuffle, repeat_mode, volume, device_id, updated_at] + properties: + track_id: + type: string + format: uuid + nullable: true + position_ms: + type: integer + queue: + type: array + items: + type: string + format: uuid + queue_index: + type: integer + context: + nullable: true + allOf: + - $ref: "#/components/schemas/PlaybackContext" + shuffle: + type: boolean + repeat_mode: + type: string + enum: ["off", one, all] + volume: + type: number + device_id: + type: string + nullable: true + updated_at: + type: string + format: date-time + nullable: true + HistoryItem: + type: object + required: [id, played_at, track] + properties: + id: + type: string + format: uuid + played_at: + type: string + format: date-time + track: + $ref: "#/components/schemas/Track" + HistoryPage: + type: object + required: [items, next_cursor] + properties: + items: + type: array + items: + $ref: "#/components/schemas/HistoryItem" + next_cursor: + type: string + nullable: true + ReactedTrack: + type: object + required: [created_at, track] + properties: + created_at: + type: string + format: date-time + track: + $ref: "#/components/schemas/Track" + ReactedTrackPage: + type: object + required: [items, total, next_cursor] + properties: + items: + type: array + items: + $ref: "#/components/schemas/ReactedTrack" + total: + type: integer + next_cursor: + type: string + nullable: true + ReactionInput: + type: object + required: [reaction] + properties: + reaction: + $ref: "#/components/schemas/Reaction" Session: type: object required: [id, device_name, client_type, created_at, last_used_at, current] diff --git a/packages/api-client/src/generated/schema.ts b/packages/api-client/src/generated/schema.ts index 5366606..5644562 100644 --- a/packages/api-client/src/generated/schema.ts +++ b/packages/api-client/src/generated/schema.ts @@ -661,6 +661,154 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/stream/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Аудио трека (поддерживает Range) + * @description Ссылку целиком даёт `stream_url` трека (подпись пользователя и срок 12 ч); клиент может добавить `q`. + * Файл отдаёт nginx через X-Accel-Redirect: оригинал из хранилища или транскод в Opus из кэша. + */ + get: operations["streamTrack"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tracks/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Треки по списку id (восстановление очереди) */ + post: operations["getTracksBatch"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Пачка событий прослушивания (до 500) + * @description Повторно присланные события (тот же id) не учитываются дважды. + */ + post: operations["postEvents"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/me/playback-state": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Последнее состояние плеера (трек, позиция, очередь) */ + get: operations["getPlaybackState"]; + /** Сохранить состояние плеера (last-write-wins) */ + put: operations["putPlaybackState"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/me/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** История прослушиваний, новые сверху */ + get: operations["getHistory"]; + put?: never; + post?: never; + /** Очистить историю (события остаются для статистики) */ + delete: operations["clearHistory"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/me/likes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Лайки, новые сверху */ + get: operations["getLikes"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/me/dislikes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Дизлайки, новые сверху */ + get: operations["getDislikes"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/me/reactions/tracks/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: components["parameters"]["EntityId"]; + }; + cookie?: never; + }; + get?: never; + /** Лайк или дизлайк трека */ + put: operations["setTrackReaction"]; + post?: never; + /** Снять реакцию */ + delete: operations["clearTrackReaction"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1009,6 +1157,9 @@ export interface components { track_no: number | null; /** @description Есть доступный файл (хранилище подключено, файл на месте) */ available: boolean; + /** @description Подписанная ссылка на аудио (12 ч); null — трек недоступен */ + stream_url: string | null; + reaction: components["schemas"]["Reaction"] | null; }; Artist: { /** Format: uuid */ @@ -1055,6 +1206,106 @@ export interface components { corrected_query: string | null; next_cursor: string | null; }; + /** @enum {string} */ + StreamQuality: "auto" | "original" | "high" | "medium" | "low"; + /** @enum {string} */ + Reaction: "like" | "dislike"; + TrackIds: { + ids: string[]; + }; + TrackList: { + items: components["schemas"]["Track"][]; + }; + PlayEvent: { + /** + * Format: uuid + * @description UUIDv7 от клиента — ключ идемпотентности + */ + id: string; + /** @enum {string} */ + type: "play_start" | "play_end" | "skip" | "seek" | "pause" | "resume" | "like" | "unlike" | "dislike" | "undislike"; + /** Format: uuid */ + track_id: string; + /** Format: date-time */ + occurred_at: string; + position_ms?: number; + played_ms?: number; + from_ms?: number; + /** @enum {string} */ + context_type?: "wave" | "album" | "artist" | "search" | "collection" | "history" | "queue"; + context_id?: string; + /** Format: uuid */ + wave_session_id?: string; + offline?: boolean; + }; + EventBatch: { + events: components["schemas"]["PlayEvent"][]; + }; + EventBatchResult: { + accepted: number; + duplicates: number; + /** @description Слишком старые (> 30 дней) или с неизвестным треком */ + rejected: number; + }; + PlaybackContext: { + type?: string; + id?: string; + label?: string; + /** Format: uuid */ + wave_session_id?: string; + }; + PlaybackStateInput: { + /** Format: uuid */ + track_id: string | null; + position_ms: number; + queue: string[]; + queue_index: number; + context?: components["schemas"]["PlaybackContext"]; + shuffle?: boolean; + /** @enum {string} */ + repeat_mode?: "off" | "one" | "all"; + volume?: number; + device_id?: string; + }; + PlaybackState: { + /** Format: uuid */ + track_id: string | null; + position_ms: number; + queue: string[]; + queue_index: number; + context: components["schemas"]["PlaybackContext"] | null; + shuffle: boolean; + /** @enum {string} */ + repeat_mode: "off" | "one" | "all"; + volume: number; + device_id: string | null; + /** Format: date-time */ + updated_at: string | null; + }; + HistoryItem: { + /** Format: uuid */ + id: string; + /** Format: date-time */ + played_at: string; + track: components["schemas"]["Track"]; + }; + HistoryPage: { + items: components["schemas"]["HistoryItem"][]; + next_cursor: string | null; + }; + ReactedTrack: { + /** Format: date-time */ + created_at: string; + track: components["schemas"]["Track"]; + }; + ReactedTrackPage: { + items: components["schemas"]["ReactedTrack"][]; + total: number; + next_cursor: string | null; + }; + ReactionInput: { + reaction: components["schemas"]["Reaction"]; + }; Session: { /** Format: uuid */ id: string; @@ -2065,4 +2316,268 @@ export interface operations { default: components["responses"]["Problem"]; }; }; + streamTrack: { + parameters: { + query: { + u: string; + exp: number; + sig: string; + q?: components["schemas"]["StreamQuality"]; + }; + header?: never; + path: { + id: components["parameters"]["EntityId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Аудио */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "audio/*": string; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getTracksBatch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrackIds"]; + }; + }; + responses: { + /** @description Треки в порядке запроса; удалённые пропущены */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrackList"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + postEvents: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EventBatch"]; + }; + }; + responses: { + /** @description Принято */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventBatchResult"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getPlaybackState: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Состояние; пусто — плеер ещё не запускался */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PlaybackState"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + putPlaybackState: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PlaybackStateInput"]; + }; + }; + responses: { + /** @description Сохранённое состояние */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PlaybackState"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getHistory: { + parameters: { + query?: { + limit?: components["parameters"]["Limit"]; + /** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */ + cursor?: components["parameters"]["Cursor"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Страница истории */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HistoryPage"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + clearHistory: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Очищено */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + getLikes: { + parameters: { + query?: { + limit?: components["parameters"]["Limit"]; + /** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */ + cursor?: components["parameters"]["Cursor"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Страница */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReactedTrackPage"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getDislikes: { + parameters: { + query?: { + limit?: components["parameters"]["Limit"]; + /** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */ + cursor?: components["parameters"]["Cursor"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Страница */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReactedTrackPage"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + setTrackReaction: { + parameters: { + query?: never; + header?: never; + path: { + id: components["parameters"]["EntityId"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReactionInput"]; + }; + }; + responses: { + /** @description Сохранено */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + clearTrackReaction: { + parameters: { + query?: never; + header?: never; + path: { + id: components["parameters"]["EntityId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Снято */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; } diff --git a/services/api/cmd/api/main.go b/services/api/cmd/api/main.go index 36558ac..f7b01ab 100644 --- a/services/api/cmd/api/main.go +++ b/services/api/cmd/api/main.go @@ -9,9 +9,11 @@ import ( "os" "os/signal" "path/filepath" + "runtime" "syscall" "time" + "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/redis/go-redis/v9" @@ -22,6 +24,7 @@ import ( "pipu/api/internal/platform/db" "pipu/api/internal/platform/httpx" "pipu/api/internal/platform/sign" + "pipu/api/internal/playback" "pipu/api/internal/search" "pipu/api/internal/server" "pipu/api/internal/system" @@ -105,6 +108,25 @@ func run() error { indexer.Trigger() } go covers.Run(ctx) // альбомы, оставшиеся без обложки с прошлого запуска + + concurrency := cfg.TranscodeConcurrency + if concurrency <= 0 { + concurrency = max(1, runtime.NumCPU()/2) + } + transcoder := playback.NewTranscoder(cfg.FFmpegPath, filepath.Join(cfg.CacheDir, "transcode"), concurrency, + cfg.TranscodeCacheLimitGB, log) + go transcoder.RunCleanup(ctx) + rootFor := func(ctx context.Context, sourceID uuid.UUID) (string, error) { + src, err := libStore.GetSource(ctx, sourceID) + if err != nil { + return "", err + } + return libSvc.Root(src) + } + var dto library.DTO + pb := playback.NewHandler(playback.NewStore(pool), libStore, func() library.DTO { return dto }, signer, transcoder, + rootFor, cfg.XAccel, log) + dto = library.DTO{Signer: signer, Personal: pb} libSvc.OnCatalogChanged = indexer.Trigger indexer.Trigger() // индекс мог отстать (новая версия, сбой Meilisearch) — пересобираем при старте go libSvc.Restore(ctx) @@ -115,8 +137,9 @@ func run() error { System: sys, Auth: auth.NewHandler(authSvc, cfg.CookieSecure), AuthService: authSvc, - Library: library.NewHandler(libSvc, libStore, signer, covers, cfg.XAccel), - Search: search.NewHandler(meili, libStore, indexer, library.DTO{Signer: signer}, log), + Library: library.NewHandler(libSvc, libStore, dto, covers, cfg.XAccel), + Search: search.NewHandler(meili, libStore, indexer, dto, log), + Playback: pb, })) log.Info("starting", "version", buildinfo.Version) return httpx.Serve(ctx, log, srv, cfg.ShutdownTimeout) diff --git a/services/api/internal/api/gen/api.gen.go b/services/api/internal/api/gen/api.gen.go index feed4d1..18dbb40 100644 --- a/services/api/internal/api/gen/api.gen.go +++ b/services/api/internal/api/gen/api.gen.go @@ -44,6 +44,51 @@ const ( HealthStatusStatusOk HealthStatusStatus = "ok" ) +// Defines values for PlayEventContextType. +const ( + PlayEventContextTypeAlbum PlayEventContextType = "album" + PlayEventContextTypeArtist PlayEventContextType = "artist" + PlayEventContextTypeCollection PlayEventContextType = "collection" + PlayEventContextTypeHistory PlayEventContextType = "history" + PlayEventContextTypeQueue PlayEventContextType = "queue" + PlayEventContextTypeSearch PlayEventContextType = "search" + PlayEventContextTypeWave PlayEventContextType = "wave" +) + +// Defines values for PlayEventType. +const ( + PlayEventTypeDislike PlayEventType = "dislike" + PlayEventTypeLike PlayEventType = "like" + PlayEventTypePause PlayEventType = "pause" + PlayEventTypePlayEnd PlayEventType = "play_end" + PlayEventTypePlayStart PlayEventType = "play_start" + PlayEventTypeResume PlayEventType = "resume" + PlayEventTypeSeek PlayEventType = "seek" + PlayEventTypeSkip PlayEventType = "skip" + PlayEventTypeUndislike PlayEventType = "undislike" + PlayEventTypeUnlike PlayEventType = "unlike" +) + +// Defines values for PlaybackStateRepeatMode. +const ( + PlaybackStateRepeatModeAll PlaybackStateRepeatMode = "all" + PlaybackStateRepeatModeOff PlaybackStateRepeatMode = "off" + PlaybackStateRepeatModeOne PlaybackStateRepeatMode = "one" +) + +// Defines values for PlaybackStateInputRepeatMode. +const ( + PlaybackStateInputRepeatModeAll PlaybackStateInputRepeatMode = "all" + PlaybackStateInputRepeatModeOff PlaybackStateInputRepeatMode = "off" + PlaybackStateInputRepeatModeOne PlaybackStateInputRepeatMode = "one" +) + +// Defines values for Reaction. +const ( + ReactionDislike Reaction = "dislike" + ReactionLike Reaction = "like" +) + // Defines values for ReadyStatusStatus. const ( ReadyStatusStatusDegraded ReadyStatusStatus = "degraded" @@ -71,6 +116,15 @@ const ( Nfs SourceType = "nfs" ) +// Defines values for StreamQuality. +const ( + Auto StreamQuality = "auto" + High StreamQuality = "high" + Low StreamQuality = "low" + Medium StreamQuality = "medium" + Original StreamQuality = "original" +) + // Defines values for TagPriority. const ( PathFirst TagPriority = "path_first" @@ -79,8 +133,8 @@ const ( // Defines values for UiPrefsMobilePlayer. const ( - Cover UiPrefsMobilePlayer = "cover" - Wave UiPrefsMobilePlayer = "wave" + UiPrefsMobilePlayerCover UiPrefsMobilePlayer = "cover" + UiPrefsMobilePlayerWave UiPrefsMobilePlayer = "wave" ) // Defines values for UiPrefsTheme. @@ -258,6 +312,20 @@ type CreatedUser struct { User AdminUser `json:"user"` } +// EventBatch defines model for EventBatch. +type EventBatch struct { + Events []PlayEvent `json:"events"` +} + +// EventBatchResult defines model for EventBatchResult. +type EventBatchResult struct { + Accepted int `json:"accepted"` + Duplicates int `json:"duplicates"` + + // Rejected Слишком старые (> 30 дней) или с неизвестным треком + Rejected int `json:"rejected"` +} + // FieldError defines model for FieldError. type FieldError struct { Code string `json:"code"` @@ -272,6 +340,19 @@ type HealthStatus struct { // HealthStatusStatus defines model for HealthStatus.Status. type HealthStatusStatus string +// HistoryItem defines model for HistoryItem. +type HistoryItem struct { + Id openapi_types.UUID `json:"id"` + PlayedAt time.Time `json:"played_at"` + Track Track `json:"track"` +} + +// HistoryPage defines model for HistoryPage. +type HistoryPage struct { + Items []HistoryItem `json:"items"` + NextCursor *string `json:"next_cursor"` +} + // HostDirList defines model for HostDirList. type HostDirList struct { Dirs []string `json:"dirs"` @@ -349,6 +430,70 @@ type PatternPreviewRequest struct { Sample *int `json:"sample,omitempty"` } +// PlayEvent defines model for PlayEvent. +type PlayEvent struct { + ContextId *string `json:"context_id,omitempty"` + ContextType *PlayEventContextType `json:"context_type,omitempty"` + FromMs *int `json:"from_ms,omitempty"` + + // Id UUIDv7 от клиента — ключ идемпотентности + Id openapi_types.UUID `json:"id"` + OccurredAt time.Time `json:"occurred_at"` + Offline *bool `json:"offline,omitempty"` + PlayedMs *int `json:"played_ms,omitempty"` + PositionMs *int `json:"position_ms,omitempty"` + TrackId openapi_types.UUID `json:"track_id"` + Type PlayEventType `json:"type"` + WaveSessionId *openapi_types.UUID `json:"wave_session_id,omitempty"` +} + +// PlayEventContextType defines model for PlayEvent.ContextType. +type PlayEventContextType string + +// PlayEventType defines model for PlayEvent.Type. +type PlayEventType string + +// PlaybackContext defines model for PlaybackContext. +type PlaybackContext struct { + Id *string `json:"id,omitempty"` + Label *string `json:"label,omitempty"` + Type *string `json:"type,omitempty"` + WaveSessionId *openapi_types.UUID `json:"wave_session_id,omitempty"` +} + +// PlaybackState defines model for PlaybackState. +type PlaybackState struct { + Context *PlaybackContext `json:"context"` + DeviceId *string `json:"device_id"` + PositionMs int `json:"position_ms"` + Queue []openapi_types.UUID `json:"queue"` + QueueIndex int `json:"queue_index"` + RepeatMode PlaybackStateRepeatMode `json:"repeat_mode"` + Shuffle bool `json:"shuffle"` + TrackId *openapi_types.UUID `json:"track_id"` + UpdatedAt *time.Time `json:"updated_at"` + Volume float32 `json:"volume"` +} + +// PlaybackStateRepeatMode defines model for PlaybackState.RepeatMode. +type PlaybackStateRepeatMode string + +// PlaybackStateInput defines model for PlaybackStateInput. +type PlaybackStateInput struct { + Context *PlaybackContext `json:"context,omitempty"` + DeviceId *string `json:"device_id,omitempty"` + PositionMs int `json:"position_ms"` + Queue []openapi_types.UUID `json:"queue"` + QueueIndex int `json:"queue_index"` + RepeatMode *PlaybackStateInputRepeatMode `json:"repeat_mode,omitempty"` + Shuffle *bool `json:"shuffle,omitempty"` + TrackId *openapi_types.UUID `json:"track_id"` + Volume *float32 `json:"volume,omitempty"` +} + +// PlaybackStateInputRepeatMode defines model for PlaybackStateInput.RepeatMode. +type PlaybackStateInputRepeatMode string + // PreviewItem defines model for PreviewItem. type PreviewItem struct { Error *string `json:"error"` @@ -377,6 +522,27 @@ type Problem struct { Type *string `json:"type,omitempty"` } +// ReactedTrack defines model for ReactedTrack. +type ReactedTrack struct { + CreatedAt time.Time `json:"created_at"` + Track Track `json:"track"` +} + +// ReactedTrackPage defines model for ReactedTrackPage. +type ReactedTrackPage struct { + Items []ReactedTrack `json:"items"` + NextCursor *string `json:"next_cursor"` + Total int `json:"total"` +} + +// Reaction defines model for Reaction. +type Reaction string + +// ReactionInput defines model for ReactionInput. +type ReactionInput struct { + Reaction Reaction `json:"reaction"` +} + // ReadyStatus defines model for ReadyStatus. type ReadyStatus struct { // Checks Результат проверки каждой зависимости @@ -550,6 +716,9 @@ type SourceTestResult struct { // SourceType defines model for SourceType. type SourceType string +// StreamQuality defines model for StreamQuality. +type StreamQuality string + // TagPriority defines model for TagPriority. type TagPriority string @@ -569,8 +738,22 @@ type Track struct { DurationMs int `json:"duration_ms"` Genres []string `json:"genres"` Id openapi_types.UUID `json:"id"` - Title string `json:"title"` - TrackNo *int `json:"track_no"` + Reaction *Reaction `json:"reaction"` + + // StreamUrl Подписанная ссылка на аудио (12 ч); null — трек недоступен + StreamUrl *string `json:"stream_url"` + Title string `json:"title"` + TrackNo *int `json:"track_no"` +} + +// TrackIds defines model for TrackIds. +type TrackIds struct { + Ids []openapi_types.UUID `json:"ids"` +} + +// TrackList defines model for TrackList. +type TrackList struct { + Items []Track `json:"items"` } // TrackMeta defines model for TrackMeta. @@ -686,6 +869,30 @@ type GetCoverParams struct { // GetCoverParamsSize defines parameters for GetCover. type GetCoverParamsSize int +// GetDislikesParams defines parameters for GetDislikes. +type GetDislikesParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Cursor Непрозрачный курсор из `next_cursor` предыдущей страницы + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` +} + +// GetHistoryParams defines parameters for GetHistory. +type GetHistoryParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Cursor Непрозрачный курсор из `next_cursor` предыдущей страницы + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` +} + +// GetLikesParams defines parameters for GetLikes. +type GetLikesParams struct { + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Cursor Непрозрачный курсор из `next_cursor` предыдущей страницы + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` +} + // SearchParams defines parameters for Search. type SearchParams struct { Q string `form:"q" json:"q"` @@ -704,6 +911,14 @@ type SearchSuggestParams struct { Q string `form:"q" json:"q"` } +// StreamTrackParams defines parameters for StreamTrack. +type StreamTrackParams struct { + U openapi_types.UUID `form:"u" json:"u"` + Exp int64 `form:"exp" json:"exp"` + Sig string `form:"sig" json:"sig"` + Q *StreamQuality `form:"q,omitempty" json:"q,omitempty"` +} + // CreateSourceJSONRequestBody defines body for CreateSource for application/json ContentType. type CreateSourceJSONRequestBody = SourceInput @@ -731,6 +946,9 @@ type LoginJSONRequestBody = LoginRequest // RefreshTokensJSONRequestBody defines body for RefreshTokens for application/json ContentType. type RefreshTokensJSONRequestBody = RefreshRequest +// PostEventsJSONRequestBody defines body for PostEvents for application/json ContentType. +type PostEventsJSONRequestBody = EventBatch + // UpdateMeJSONRequestBody defines body for UpdateMe for application/json ContentType. type UpdateMeJSONRequestBody = UpdateMeRequest @@ -740,6 +958,15 @@ type SetBootstrapCredentialsJSONRequestBody = BootstrapCredentialsRequest // ChangePasswordJSONRequestBody defines body for ChangePassword for application/json ContentType. type ChangePasswordJSONRequestBody = ChangePasswordRequest +// PutPlaybackStateJSONRequestBody defines body for PutPlaybackState for application/json ContentType. +type PutPlaybackStateJSONRequestBody = PlaybackStateInput + +// SetTrackReactionJSONRequestBody defines body for SetTrackReaction for application/json ContentType. +type SetTrackReactionJSONRequestBody = ReactionInput + +// GetTracksBatchJSONRequestBody defines body for GetTracksBatch for application/json ContentType. +type GetTracksBatchJSONRequestBody = TrackIds + // ServerInterface represents all server handlers. type ServerInterface interface { // Журнал действий (admin), новые сверху @@ -826,6 +1053,9 @@ type ServerInterface interface { // Обложка альбома (WebP) // (GET /api/v1/covers/{id}) GetCover(w http.ResponseWriter, r *http.Request, id EntityId, params GetCoverParams) + // Пачка событий прослушивания (до 500) + // (POST /api/v1/events) + PostEvents(w http.ResponseWriter, r *http.Request) // Текущий пользователь // (GET /api/v1/me) GetMe(w http.ResponseWriter, r *http.Request) @@ -835,9 +1065,33 @@ type ServerInterface interface { // Первый запуск — новые логин и пароль администратора // (POST /api/v1/me/bootstrap-credentials) SetBootstrapCredentials(w http.ResponseWriter, r *http.Request) + // Дизлайки, новые сверху + // (GET /api/v1/me/dislikes) + GetDislikes(w http.ResponseWriter, r *http.Request, params GetDislikesParams) + // Очистить историю (события остаются для статистики) + // (DELETE /api/v1/me/history) + ClearHistory(w http.ResponseWriter, r *http.Request) + // История прослушиваний, новые сверху + // (GET /api/v1/me/history) + GetHistory(w http.ResponseWriter, r *http.Request, params GetHistoryParams) + // Лайки, новые сверху + // (GET /api/v1/me/likes) + GetLikes(w http.ResponseWriter, r *http.Request, params GetLikesParams) // Сменить пароль // (POST /api/v1/me/password) ChangePassword(w http.ResponseWriter, r *http.Request) + // Последнее состояние плеера (трек, позиция, очередь) + // (GET /api/v1/me/playback-state) + GetPlaybackState(w http.ResponseWriter, r *http.Request) + // Сохранить состояние плеера (last-write-wins) + // (PUT /api/v1/me/playback-state) + PutPlaybackState(w http.ResponseWriter, r *http.Request) + // Снять реакцию + // (DELETE /api/v1/me/reactions/tracks/{id}) + ClearTrackReaction(w http.ResponseWriter, r *http.Request, id EntityId) + // Лайк или дизлайк трека + // (PUT /api/v1/me/reactions/tracks/{id}) + SetTrackReaction(w http.ResponseWriter, r *http.Request, id EntityId) // Активные сессии (устройства) // (GET /api/v1/me/sessions) ListMySessions(w http.ResponseWriter, r *http.Request) @@ -853,6 +1107,12 @@ type ServerInterface interface { // Сведения о сервере для клиентов // (GET /api/v1/server-info) GetServerInfo(w http.ResponseWriter, r *http.Request) + // Аудио трека (поддерживает Range) + // (GET /api/v1/stream/{id}) + StreamTrack(w http.ResponseWriter, r *http.Request, id EntityId, params StreamTrackParams) + // Треки по списку id (восстановление очереди) + // (POST /api/v1/tracks/batch) + GetTracksBatch(w http.ResponseWriter, r *http.Request) // Трек // (GET /api/v1/tracks/{id}) GetTrack(w http.ResponseWriter, r *http.Request, id EntityId) @@ -1036,6 +1296,12 @@ func (_ Unimplemented) GetCover(w http.ResponseWriter, r *http.Request, id Entit w.WriteHeader(http.StatusNotImplemented) } +// Пачка событий прослушивания (до 500) +// (POST /api/v1/events) +func (_ Unimplemented) PostEvents(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Текущий пользователь // (GET /api/v1/me) func (_ Unimplemented) GetMe(w http.ResponseWriter, r *http.Request) { @@ -1054,12 +1320,60 @@ func (_ Unimplemented) SetBootstrapCredentials(w http.ResponseWriter, r *http.Re w.WriteHeader(http.StatusNotImplemented) } +// Дизлайки, новые сверху +// (GET /api/v1/me/dislikes) +func (_ Unimplemented) GetDislikes(w http.ResponseWriter, r *http.Request, params GetDislikesParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Очистить историю (события остаются для статистики) +// (DELETE /api/v1/me/history) +func (_ Unimplemented) ClearHistory(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// История прослушиваний, новые сверху +// (GET /api/v1/me/history) +func (_ Unimplemented) GetHistory(w http.ResponseWriter, r *http.Request, params GetHistoryParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Лайки, новые сверху +// (GET /api/v1/me/likes) +func (_ Unimplemented) GetLikes(w http.ResponseWriter, r *http.Request, params GetLikesParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Сменить пароль // (POST /api/v1/me/password) func (_ Unimplemented) ChangePassword(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Последнее состояние плеера (трек, позиция, очередь) +// (GET /api/v1/me/playback-state) +func (_ Unimplemented) GetPlaybackState(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Сохранить состояние плеера (last-write-wins) +// (PUT /api/v1/me/playback-state) +func (_ Unimplemented) PutPlaybackState(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Снять реакцию +// (DELETE /api/v1/me/reactions/tracks/{id}) +func (_ Unimplemented) ClearTrackReaction(w http.ResponseWriter, r *http.Request, id EntityId) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Лайк или дизлайк трека +// (PUT /api/v1/me/reactions/tracks/{id}) +func (_ Unimplemented) SetTrackReaction(w http.ResponseWriter, r *http.Request, id EntityId) { + w.WriteHeader(http.StatusNotImplemented) +} + // Активные сессии (устройства) // (GET /api/v1/me/sessions) func (_ Unimplemented) ListMySessions(w http.ResponseWriter, r *http.Request) { @@ -1090,6 +1404,18 @@ func (_ Unimplemented) GetServerInfo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Аудио трека (поддерживает Range) +// (GET /api/v1/stream/{id}) +func (_ Unimplemented) StreamTrack(w http.ResponseWriter, r *http.Request, id EntityId, params StreamTrackParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Треки по списку id (восстановление очереди) +// (POST /api/v1/tracks/batch) +func (_ Unimplemented) GetTracksBatch(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Трек // (GET /api/v1/tracks/{id}) func (_ Unimplemented) GetTrack(w http.ResponseWriter, r *http.Request, id EntityId) { @@ -1977,6 +2303,26 @@ func (siw *ServerInterfaceWrapper) GetCover(w http.ResponseWriter, r *http.Reque handler.ServeHTTP(w, r) } +// PostEvents operation middleware +func (siw *ServerInterfaceWrapper) PostEvents(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostEvents(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetMe operation middleware func (siw *ServerInterfaceWrapper) GetMe(w http.ResponseWriter, r *http.Request) { @@ -2037,6 +2383,149 @@ func (siw *ServerInterfaceWrapper) SetBootstrapCredentials(w http.ResponseWriter handler.ServeHTTP(w, r) } +// GetDislikes operation middleware +func (siw *ServerInterfaceWrapper) GetDislikes(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetDislikesParams + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", r.URL.Query(), ¶ms.Cursor) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "cursor", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetDislikes(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// ClearHistory operation middleware +func (siw *ServerInterfaceWrapper) ClearHistory(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ClearHistory(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetHistory operation middleware +func (siw *ServerInterfaceWrapper) GetHistory(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetHistoryParams + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", r.URL.Query(), ¶ms.Cursor) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "cursor", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetHistory(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetLikes operation middleware +func (siw *ServerInterfaceWrapper) GetLikes(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetLikesParams + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "cursor" ------------- + + err = runtime.BindQueryParameter("form", true, false, "cursor", r.URL.Query(), ¶ms.Cursor) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "cursor", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetLikes(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ChangePassword operation middleware func (siw *ServerInterfaceWrapper) ChangePassword(w http.ResponseWriter, r *http.Request) { @@ -2057,6 +2546,108 @@ func (siw *ServerInterfaceWrapper) ChangePassword(w http.ResponseWriter, r *http handler.ServeHTTP(w, r) } +// GetPlaybackState operation middleware +func (siw *ServerInterfaceWrapper) GetPlaybackState(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetPlaybackState(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PutPlaybackState operation middleware +func (siw *ServerInterfaceWrapper) PutPlaybackState(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PutPlaybackState(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// ClearTrackReaction operation middleware +func (siw *ServerInterfaceWrapper) ClearTrackReaction(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id EntityId + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ClearTrackReaction(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// SetTrackReaction operation middleware +func (siw *ServerInterfaceWrapper) SetTrackReaction(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id EntityId + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SetTrackReaction(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListMySessions operation middleware func (siw *ServerInterfaceWrapper) ListMySessions(w http.ResponseWriter, r *http.Request) { @@ -2226,6 +2817,107 @@ func (siw *ServerInterfaceWrapper) GetServerInfo(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } +// StreamTrack operation middleware +func (siw *ServerInterfaceWrapper) StreamTrack(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id EntityId + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params StreamTrackParams + + // ------------- Required query parameter "u" ------------- + + if paramValue := r.URL.Query().Get("u"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "u"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "u", r.URL.Query(), ¶ms.U) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "u", Err: err}) + return + } + + // ------------- Required query parameter "exp" ------------- + + if paramValue := r.URL.Query().Get("exp"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "exp"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "exp", r.URL.Query(), ¶ms.Exp) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "exp", Err: err}) + return + } + + // ------------- Required query parameter "sig" ------------- + + if paramValue := r.URL.Query().Get("sig"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "sig"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "sig", r.URL.Query(), ¶ms.Sig) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sig", Err: err}) + return + } + + // ------------- Optional query parameter "q" ------------- + + err = runtime.BindQueryParameter("form", true, false, "q", r.URL.Query(), ¶ms.Q) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "q", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StreamTrack(w, r, id, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetTracksBatch operation middleware +func (siw *ServerInterfaceWrapper) GetTracksBatch(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTracksBatch(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetTrack operation middleware func (siw *ServerInterfaceWrapper) GetTrack(w http.ResponseWriter, r *http.Request) { @@ -2482,6 +3174,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/covers/{id}", wrapper.GetCover) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/api/v1/events", wrapper.PostEvents) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/me", wrapper.GetMe) }) @@ -2491,9 +3186,33 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/me/bootstrap-credentials", wrapper.SetBootstrapCredentials) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/me/dislikes", wrapper.GetDislikes) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/api/v1/me/history", wrapper.ClearHistory) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/me/history", wrapper.GetHistory) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/me/likes", wrapper.GetLikes) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/me/password", wrapper.ChangePassword) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/me/playback-state", wrapper.GetPlaybackState) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/api/v1/me/playback-state", wrapper.PutPlaybackState) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/api/v1/me/reactions/tracks/{id}", wrapper.ClearTrackReaction) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/api/v1/me/reactions/tracks/{id}", wrapper.SetTrackReaction) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/me/sessions", wrapper.ListMySessions) }) @@ -2509,6 +3228,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/server-info", wrapper.GetServerInfo) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/stream/{id}", wrapper.StreamTrack) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/api/v1/tracks/batch", wrapper.GetTracksBatch) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/tracks/{id}", wrapper.GetTrack) }) @@ -3343,6 +4068,35 @@ func (response GetCoverdefaultApplicationProblemPlusJSONResponse) VisitGetCoverR return json.NewEncoder(w).Encode(response.Body) } +type PostEventsRequestObject struct { + Body *PostEventsJSONRequestBody +} + +type PostEventsResponseObject interface { + VisitPostEventsResponse(w http.ResponseWriter) error +} + +type PostEvents200JSONResponse EventBatchResult + +func (response PostEvents200JSONResponse) VisitPostEventsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PostEventsdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response PostEventsdefaultApplicationProblemPlusJSONResponse) VisitPostEventsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type GetMeRequestObject struct { } @@ -3429,6 +4183,120 @@ func (response SetBootstrapCredentialsdefaultApplicationProblemPlusJSONResponse) return json.NewEncoder(w).Encode(response.Body) } +type GetDislikesRequestObject struct { + Params GetDislikesParams +} + +type GetDislikesResponseObject interface { + VisitGetDislikesResponse(w http.ResponseWriter) error +} + +type GetDislikes200JSONResponse ReactedTrackPage + +func (response GetDislikes200JSONResponse) VisitGetDislikesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetDislikesdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetDislikesdefaultApplicationProblemPlusJSONResponse) VisitGetDislikesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type ClearHistoryRequestObject struct { +} + +type ClearHistoryResponseObject interface { + VisitClearHistoryResponse(w http.ResponseWriter) error +} + +type ClearHistory204Response struct { +} + +func (response ClearHistory204Response) VisitClearHistoryResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type ClearHistorydefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response ClearHistorydefaultApplicationProblemPlusJSONResponse) VisitClearHistoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type GetHistoryRequestObject struct { + Params GetHistoryParams +} + +type GetHistoryResponseObject interface { + VisitGetHistoryResponse(w http.ResponseWriter) error +} + +type GetHistory200JSONResponse HistoryPage + +func (response GetHistory200JSONResponse) VisitGetHistoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetHistorydefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetHistorydefaultApplicationProblemPlusJSONResponse) VisitGetHistoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type GetLikesRequestObject struct { + Params GetLikesParams +} + +type GetLikesResponseObject interface { + VisitGetLikesResponse(w http.ResponseWriter) error +} + +type GetLikes200JSONResponse ReactedTrackPage + +func (response GetLikes200JSONResponse) VisitGetLikesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetLikesdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetLikesdefaultApplicationProblemPlusJSONResponse) VisitGetLikesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type ChangePasswordRequestObject struct { Body *ChangePasswordJSONRequestBody } @@ -3458,6 +4326,120 @@ func (response ChangePassworddefaultApplicationProblemPlusJSONResponse) VisitCha return json.NewEncoder(w).Encode(response.Body) } +type GetPlaybackStateRequestObject struct { +} + +type GetPlaybackStateResponseObject interface { + VisitGetPlaybackStateResponse(w http.ResponseWriter) error +} + +type GetPlaybackState200JSONResponse PlaybackState + +func (response GetPlaybackState200JSONResponse) VisitGetPlaybackStateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetPlaybackStatedefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetPlaybackStatedefaultApplicationProblemPlusJSONResponse) VisitGetPlaybackStateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type PutPlaybackStateRequestObject struct { + Body *PutPlaybackStateJSONRequestBody +} + +type PutPlaybackStateResponseObject interface { + VisitPutPlaybackStateResponse(w http.ResponseWriter) error +} + +type PutPlaybackState200JSONResponse PlaybackState + +func (response PutPlaybackState200JSONResponse) VisitPutPlaybackStateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PutPlaybackStatedefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response PutPlaybackStatedefaultApplicationProblemPlusJSONResponse) VisitPutPlaybackStateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type ClearTrackReactionRequestObject struct { + Id EntityId `json:"id"` +} + +type ClearTrackReactionResponseObject interface { + VisitClearTrackReactionResponse(w http.ResponseWriter) error +} + +type ClearTrackReaction204Response struct { +} + +func (response ClearTrackReaction204Response) VisitClearTrackReactionResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type ClearTrackReactiondefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response ClearTrackReactiondefaultApplicationProblemPlusJSONResponse) VisitClearTrackReactionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type SetTrackReactionRequestObject struct { + Id EntityId `json:"id"` + Body *SetTrackReactionJSONRequestBody +} + +type SetTrackReactionResponseObject interface { + VisitSetTrackReactionResponse(w http.ResponseWriter) error +} + +type SetTrackReaction204Response struct { +} + +func (response SetTrackReaction204Response) VisitSetTrackReactionResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type SetTrackReactiondefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response SetTrackReactiondefaultApplicationProblemPlusJSONResponse) VisitSetTrackReactionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type ListMySessionsRequestObject struct { } @@ -3600,6 +4582,76 @@ func (response GetServerInfodefaultApplicationProblemPlusJSONResponse) VisitGetS return json.NewEncoder(w).Encode(response.Body) } +type StreamTrackRequestObject struct { + Id EntityId `json:"id"` + Params StreamTrackParams +} + +type StreamTrackResponseObject interface { + VisitStreamTrackResponse(w http.ResponseWriter) error +} + +type StreamTrack200AudioResponse struct { + Body io.Reader + ContentType string + ContentLength int64 +} + +func (response StreamTrack200AudioResponse) VisitStreamTrackResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", response.ContentType) + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type StreamTrackdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response StreamTrackdefaultApplicationProblemPlusJSONResponse) VisitStreamTrackResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type GetTracksBatchRequestObject struct { + Body *GetTracksBatchJSONRequestBody +} + +type GetTracksBatchResponseObject interface { + VisitGetTracksBatchResponse(w http.ResponseWriter) error +} + +type GetTracksBatch200JSONResponse TrackList + +func (response GetTracksBatch200JSONResponse) VisitGetTracksBatchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetTracksBatchdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetTracksBatchdefaultApplicationProblemPlusJSONResponse) VisitGetTracksBatchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type GetTrackRequestObject struct { Id EntityId `json:"id"` } @@ -3756,6 +4808,9 @@ type StrictServerInterface interface { // Обложка альбома (WebP) // (GET /api/v1/covers/{id}) GetCover(ctx context.Context, request GetCoverRequestObject) (GetCoverResponseObject, error) + // Пачка событий прослушивания (до 500) + // (POST /api/v1/events) + PostEvents(ctx context.Context, request PostEventsRequestObject) (PostEventsResponseObject, error) // Текущий пользователь // (GET /api/v1/me) GetMe(ctx context.Context, request GetMeRequestObject) (GetMeResponseObject, error) @@ -3765,9 +4820,33 @@ type StrictServerInterface interface { // Первый запуск — новые логин и пароль администратора // (POST /api/v1/me/bootstrap-credentials) SetBootstrapCredentials(ctx context.Context, request SetBootstrapCredentialsRequestObject) (SetBootstrapCredentialsResponseObject, error) + // Дизлайки, новые сверху + // (GET /api/v1/me/dislikes) + GetDislikes(ctx context.Context, request GetDislikesRequestObject) (GetDislikesResponseObject, error) + // Очистить историю (события остаются для статистики) + // (DELETE /api/v1/me/history) + ClearHistory(ctx context.Context, request ClearHistoryRequestObject) (ClearHistoryResponseObject, error) + // История прослушиваний, новые сверху + // (GET /api/v1/me/history) + GetHistory(ctx context.Context, request GetHistoryRequestObject) (GetHistoryResponseObject, error) + // Лайки, новые сверху + // (GET /api/v1/me/likes) + GetLikes(ctx context.Context, request GetLikesRequestObject) (GetLikesResponseObject, error) // Сменить пароль // (POST /api/v1/me/password) ChangePassword(ctx context.Context, request ChangePasswordRequestObject) (ChangePasswordResponseObject, error) + // Последнее состояние плеера (трек, позиция, очередь) + // (GET /api/v1/me/playback-state) + GetPlaybackState(ctx context.Context, request GetPlaybackStateRequestObject) (GetPlaybackStateResponseObject, error) + // Сохранить состояние плеера (last-write-wins) + // (PUT /api/v1/me/playback-state) + PutPlaybackState(ctx context.Context, request PutPlaybackStateRequestObject) (PutPlaybackStateResponseObject, error) + // Снять реакцию + // (DELETE /api/v1/me/reactions/tracks/{id}) + ClearTrackReaction(ctx context.Context, request ClearTrackReactionRequestObject) (ClearTrackReactionResponseObject, error) + // Лайк или дизлайк трека + // (PUT /api/v1/me/reactions/tracks/{id}) + SetTrackReaction(ctx context.Context, request SetTrackReactionRequestObject) (SetTrackReactionResponseObject, error) // Активные сессии (устройства) // (GET /api/v1/me/sessions) ListMySessions(ctx context.Context, request ListMySessionsRequestObject) (ListMySessionsResponseObject, error) @@ -3783,6 +4862,12 @@ type StrictServerInterface interface { // Сведения о сервере для клиентов // (GET /api/v1/server-info) GetServerInfo(ctx context.Context, request GetServerInfoRequestObject) (GetServerInfoResponseObject, error) + // Аудио трека (поддерживает Range) + // (GET /api/v1/stream/{id}) + StreamTrack(ctx context.Context, request StreamTrackRequestObject) (StreamTrackResponseObject, error) + // Треки по списку id (восстановление очереди) + // (POST /api/v1/tracks/batch) + GetTracksBatch(ctx context.Context, request GetTracksBatchRequestObject) (GetTracksBatchResponseObject, error) // Трек // (GET /api/v1/tracks/{id}) GetTrack(ctx context.Context, request GetTrackRequestObject) (GetTrackResponseObject, error) @@ -4599,6 +5684,37 @@ func (sh *strictHandler) GetCover(w http.ResponseWriter, r *http.Request, id Ent } } +// PostEvents operation middleware +func (sh *strictHandler) PostEvents(w http.ResponseWriter, r *http.Request) { + var request PostEventsRequestObject + + var body PostEventsJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostEvents(ctx, request.(PostEventsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostEvents") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostEventsResponseObject); ok { + if err := validResponse.VisitPostEventsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetMe operation middleware func (sh *strictHandler) GetMe(w http.ResponseWriter, r *http.Request) { var request GetMeRequestObject @@ -4685,6 +5801,108 @@ func (sh *strictHandler) SetBootstrapCredentials(w http.ResponseWriter, r *http. } } +// GetDislikes operation middleware +func (sh *strictHandler) GetDislikes(w http.ResponseWriter, r *http.Request, params GetDislikesParams) { + var request GetDislikesRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDislikes(ctx, request.(GetDislikesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDislikes") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDislikesResponseObject); ok { + if err := validResponse.VisitGetDislikesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ClearHistory operation middleware +func (sh *strictHandler) ClearHistory(w http.ResponseWriter, r *http.Request) { + var request ClearHistoryRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ClearHistory(ctx, request.(ClearHistoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ClearHistory") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ClearHistoryResponseObject); ok { + if err := validResponse.VisitClearHistoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetHistory operation middleware +func (sh *strictHandler) GetHistory(w http.ResponseWriter, r *http.Request, params GetHistoryParams) { + var request GetHistoryRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetHistory(ctx, request.(GetHistoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetHistory") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetHistoryResponseObject); ok { + if err := validResponse.VisitGetHistoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetLikes operation middleware +func (sh *strictHandler) GetLikes(w http.ResponseWriter, r *http.Request, params GetLikesParams) { + var request GetLikesRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetLikes(ctx, request.(GetLikesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetLikes") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetLikesResponseObject); ok { + if err := validResponse.VisitGetLikesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ChangePassword operation middleware func (sh *strictHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { var request ChangePasswordRequestObject @@ -4716,6 +5934,120 @@ func (sh *strictHandler) ChangePassword(w http.ResponseWriter, r *http.Request) } } +// GetPlaybackState operation middleware +func (sh *strictHandler) GetPlaybackState(w http.ResponseWriter, r *http.Request) { + var request GetPlaybackStateRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetPlaybackState(ctx, request.(GetPlaybackStateRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetPlaybackState") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetPlaybackStateResponseObject); ok { + if err := validResponse.VisitGetPlaybackStateResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PutPlaybackState operation middleware +func (sh *strictHandler) PutPlaybackState(w http.ResponseWriter, r *http.Request) { + var request PutPlaybackStateRequestObject + + var body PutPlaybackStateJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PutPlaybackState(ctx, request.(PutPlaybackStateRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PutPlaybackState") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PutPlaybackStateResponseObject); ok { + if err := validResponse.VisitPutPlaybackStateResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ClearTrackReaction operation middleware +func (sh *strictHandler) ClearTrackReaction(w http.ResponseWriter, r *http.Request, id EntityId) { + var request ClearTrackReactionRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ClearTrackReaction(ctx, request.(ClearTrackReactionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ClearTrackReaction") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ClearTrackReactionResponseObject); ok { + if err := validResponse.VisitClearTrackReactionResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// SetTrackReaction operation middleware +func (sh *strictHandler) SetTrackReaction(w http.ResponseWriter, r *http.Request, id EntityId) { + var request SetTrackReactionRequestObject + + request.Id = id + + var body SetTrackReactionJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.SetTrackReaction(ctx, request.(SetTrackReactionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SetTrackReaction") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(SetTrackReactionResponseObject); ok { + if err := validResponse.VisitSetTrackReactionResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ListMySessions operation middleware func (sh *strictHandler) ListMySessions(w http.ResponseWriter, r *http.Request) { var request ListMySessionsRequestObject @@ -4842,6 +6174,64 @@ func (sh *strictHandler) GetServerInfo(w http.ResponseWriter, r *http.Request) { } } +// StreamTrack operation middleware +func (sh *strictHandler) StreamTrack(w http.ResponseWriter, r *http.Request, id EntityId, params StreamTrackParams) { + var request StreamTrackRequestObject + + request.Id = id + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StreamTrack(ctx, request.(StreamTrackRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamTrack") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StreamTrackResponseObject); ok { + if err := validResponse.VisitStreamTrackResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetTracksBatch operation middleware +func (sh *strictHandler) GetTracksBatch(w http.ResponseWriter, r *http.Request) { + var request GetTracksBatchRequestObject + + var body GetTracksBatchJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetTracksBatch(ctx, request.(GetTracksBatchRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetTracksBatch") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetTracksBatchResponseObject); ok { + if err := validResponse.VisitGetTracksBatchResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetTrack operation middleware func (sh *strictHandler) GetTrack(w http.ResponseWriter, r *http.Request, id EntityId) { var request GetTrackRequestObject @@ -4919,137 +6309,162 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/services/api/internal/auth/middleware.go b/services/api/internal/auth/middleware.go index c1558ac..0241010 100644 --- a/services/api/internal/auth/middleware.go +++ b/services/api/internal/auth/middleware.go @@ -88,7 +88,9 @@ func Middleware(svc *Service, log *slog.Logger) func(http.Handler) http.Handler r.ContentLength = 2 } // Обложки открывает без заголовка Authorization — ссылку защищает подпись. - cover := r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v1/covers/") + // Аудио открывает