Transcoder: Pick Audio Quality (#1353)

This commit is contained in:
Zoe Roux 2026-03-28 15:55:50 +01:00 committed by GitHub
commit 7673416cf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2882 additions and 365 deletions

View File

@ -42,6 +42,26 @@ const docTemplate = `{
}
}
},
"/info": {
"get": {
"description": "List keibi's settings (oidc providers, public url...)",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "Auth info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.ServerInfo"
}
}
}
}
},
"/jwt": {
"get": {
"security": [
@ -199,7 +219,252 @@ const docTemplate = `{
}
}
},
"/oidc/callback/{provider}": {
"get": {
"description": "Exchange an opaque OIDC token for a local session.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC callback",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Opaque token returned by /oidc/logged/:provider",
"name": "token",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Optional tenant passthrough for federated setups",
"name": "tenant",
"in": "query"
},
{
"type": "string",
"description": "Bearer token to link provider to current account",
"name": "Authorization",
"in": "header"
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/main.SessionWToken"
}
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"410": {
"description": "Login token expired or already used",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/oidc/logged/{provider}": {
"get": {
"description": "Callback endpoint called by OIDC providers after login.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC logged callback",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
},
{
"type": "string",
"description": "State value returned by the provider",
"name": "state",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Authorization code",
"name": "code",
"in": "query"
},
{
"type": "string",
"description": "Provider callback error",
"name": "error",
"in": "query"
}
],
"responses": {
"302": {
"description": "Found"
},
"400": {
"description": "Invalid state",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/oidc/login/{provider}": {
"get": {
"description": "Start an OIDC login with a provider.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC login",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
},
{
"type": "string",
"description": "URL to redirect the browser to after provider callback",
"name": "redirectUrl",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Optional tenant passthrough for federated setups",
"name": "tenant",
"in": "query"
}
],
"responses": {
"302": {
"description": "Found"
},
"400": {
"description": "Missing redirectUrl",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"delete": {
"security": [
{
"Jwt": []
}
],
"description": "Remove an OIDC provider from the current account.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC unlink provider",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/sessions": {
"get": {
"security": [
{
"Jwt": []
}
],
"description": "List all active sessions for the currently connected user",
"produces": [
"application/json"
],
"tags": [
"sessions"
],
"summary": "List my sessions",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/main.SessionWCurrent"
}
}
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"post": {
"description": "Login to your account and open a session",
"consumes": [
@ -374,7 +639,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.Page-main_User"
"$ref": "#/definitions/main.Page-models_User"
}
},
"422": {
@ -410,7 +675,7 @@ const docTemplate = `{
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.RegisterDto"
"$ref": "#/definitions/models.RegisterDto"
}
}
],
@ -421,6 +686,12 @@ const docTemplate = `{
"$ref": "#/definitions/main.SessionWToken"
}
},
"403": {
"description": "Registrations are disabled",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"409": {
"description": "Duplicated email or username",
"schema": {
@ -455,7 +726,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"401": {
@ -493,7 +764,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
}
}
@ -521,7 +792,7 @@ const docTemplate = `{
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
"$ref": "#/definitions/models.EditUserDto"
}
}
],
@ -529,7 +800,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"403": {
@ -547,6 +818,137 @@ const docTemplate = `{
}
}
},
"/users/me/logo": {
"get": {
"security": [
{
"Jwt": []
}
],
"description": "Get the current user's logo (manual upload if available, gravatar otherwise)",
"produces": [
"image/*"
],
"tags": [
"users"
],
"summary": "Get my logo",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "No gravatar image found for this user",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"post": {
"security": [
{
"Jwt": []
}
],
"description": "Upload a manual profile picture for the current user",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Upload my logo",
"parameters": [
{
"type": "file",
"description": "Profile picture image (jpeg/png/gif/webp, max 5MB)",
"name": "logo",
"in": "formData",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"413": {
"description": "File too large",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"422": {
"description": "Missing or invalid logo file",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"delete": {
"security": [
{
"Jwt": []
}
],
"description": "Delete the current user's manually uploaded profile picture",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Delete my logo",
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/me/password": {
"patch": {
"security": [
@ -578,7 +980,7 @@ const docTemplate = `{
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditPasswordDto"
"$ref": "#/definitions/models.EditPasswordDto"
}
}
],
@ -595,6 +997,49 @@ const docTemplate = `{
}
}
},
"/users/me/{id}": {
"delete": {
"security": [
{
"Jwt": []
}
],
"description": "Delete the user's manually uploaded profile picture",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Delete user logo",
"parameters": [
{
"type": "string",
"description": "The id or username of the user",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/{id}": {
"get": {
"security": [
@ -626,7 +1071,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"404": {
@ -675,7 +1120,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"404": {
@ -724,7 +1169,7 @@ const docTemplate = `{
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
"$ref": "#/definitions/models.EditUserDto"
}
}
],
@ -732,7 +1177,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"403": {
@ -749,6 +1194,104 @@ const docTemplate = `{
}
}
}
},
"/users/{id}/logo": {
"get": {
"security": [
{
"Jwt": [
"users.read"
]
}
],
"description": "Get a user's logo (manual upload if available, gravatar otherwise)",
"produces": [
"image/*"
],
"tags": [
"users"
],
"summary": "Get user logo",
"parameters": [
{
"type": "string",
"description": "The id or username of the user",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "No gravatar image found for this user",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/{id}/sessions": {
"get": {
"security": [
{
"Jwt": []
}
],
"description": "List all active sessions for a user. Listing someone else's sessions requires users.read.",
"produces": [
"application/json"
],
"tags": [
"sessions"
],
"summary": "List user sessions",
"parameters": [
{
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397",
"description": "The id or username of the user",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/main.Session"
}
}
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Missing permissions: users.read.",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "No user found with id or username",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
}
},
"definitions": {
@ -834,40 +1377,6 @@ const docTemplate = `{
}
}
},
"main.EditPasswordDto": {
"type": "object",
"required": [
"password"
],
"properties": {
"password": {
"type": "string",
"example": "password1234"
}
}
},
"main.EditUserDto": {
"type": "object",
"properties": {
"claims": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"preferOriginal": " true"
}
},
"email": {
"type": "string",
"example": "kyoo@zoriya.dev"
},
"username": {
"type": "string",
"example": "zoriya"
}
}
},
"main.JwkSet": {
"type": "object",
"properties": {
@ -949,24 +1458,14 @@ const docTemplate = `{
}
}
},
"main.OidcHandle": {
"main.OidcInfo": {
"type": "object",
"properties": {
"id": {
"description": "Id of this oidc handle.",
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
"logo": {
"type": "string"
},
"profileUrl": {
"description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.",
"type": "string",
"format": "url",
"example": "https://myanimelist.net/profile/zoriya"
},
"username": {
"description": "Username of the user on the external service.",
"type": "string",
"example": "zoriya"
"name": {
"type": "string"
}
}
},
@ -989,13 +1488,13 @@ const docTemplate = `{
}
}
},
"main.Page-main_User": {
"main.Page-models_User": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"next": {
@ -1008,29 +1507,20 @@ const docTemplate = `{
}
}
},
"main.RegisterDto": {
"main.ServerInfo": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"description": "Valid email that could be used for forgotten password requests. Can be used for login.",
"type": "string",
"format": "email",
"example": "kyoo@zoriya.dev"
"allowRegister": {
"type": "boolean"
},
"password": {
"description": "Password to use.",
"type": "string",
"example": "password1234"
"oidc": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/main.OidcInfo"
}
},
"username": {
"description": "Username of the new account, can't contain @ signs. Can be used for login.",
"type": "string",
"example": "zoriya"
"publicUrl": {
"type": "string"
}
}
},
@ -1059,6 +1549,34 @@ const docTemplate = `{
}
}
},
"main.SessionWCurrent": {
"type": "object",
"properties": {
"createdDate": {
"description": "When was the session first opened",
"type": "string",
"example": "2025-03-29T18:20:05.267Z"
},
"current": {
"type": "boolean"
},
"device": {
"description": "Device that created the session.",
"type": "string",
"example": "Web - Firefox"
},
"id": {
"description": "Unique id of this session. Can be used for calls to DELETE",
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
},
"lastUsed": {
"description": "Last date this session was used to access a service.",
"type": "string",
"example": "2025-03-29T18:20:05.267Z"
}
}
},
"main.SessionWToken": {
"type": "object",
"properties": {
@ -1088,7 +1606,92 @@ const docTemplate = `{
}
}
},
"main.User": {
"models.EditPasswordDto": {
"type": "object",
"required": [
"newPassword"
],
"properties": {
"newPassword": {
"type": "string",
"example": "password1234"
},
"oldPassword": {
"type": "string",
"example": "password1234"
}
}
},
"models.EditUserDto": {
"type": "object",
"properties": {
"claims": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"preferOriginal": " true"
}
},
"email": {
"type": "string",
"example": "kyoo@zoriya.dev"
},
"username": {
"type": "string",
"example": "zoriya"
}
}
},
"models.OidcHandle": {
"type": "object",
"properties": {
"id": {
"description": "Id of this oidc handle.",
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
},
"profileUrl": {
"description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.",
"type": "string",
"format": "url",
"example": "https://myanimelist.net/profile/zoriya"
},
"username": {
"description": "Username of the user on the external service.",
"type": "string",
"example": "zoriya"
}
}
},
"models.RegisterDto": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"description": "Valid email that could be used for forgotten password requests. Can be used for login.",
"type": "string",
"format": "email",
"example": "kyoo@zoriya.dev"
},
"password": {
"description": "Password to use.",
"type": "string",
"example": "password1234"
},
"username": {
"description": "Username of the new account, can't contain @ signs. Can be used for login.",
"type": "string",
"example": "zoriya"
}
}
},
"models.User": {
"type": "object",
"properties": {
"claims": {
@ -1112,6 +1715,10 @@ const docTemplate = `{
"format": "email",
"example": "kyoo@zoriya.dev"
},
"hasPassword": {
"description": "False if the user has never setup a password and only used oidc.",
"type": "boolean"
},
"id": {
"description": "Id of the user.",
"type": "string",
@ -1126,7 +1733,7 @@ const docTemplate = `{
"description": "List of other login method available for this user. Access tokens wont be returned here.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/main.OidcHandle"
"$ref": "#/definitions/models.OidcHandle"
}
},
"username": {

View File

@ -36,6 +36,26 @@
}
}
},
"/info": {
"get": {
"description": "List keibi's settings (oidc providers, public url...)",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "Auth info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.ServerInfo"
}
}
}
}
},
"/jwt": {
"get": {
"security": [
@ -193,7 +213,252 @@
}
}
},
"/oidc/callback/{provider}": {
"get": {
"description": "Exchange an opaque OIDC token for a local session.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC callback",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Opaque token returned by /oidc/logged/:provider",
"name": "token",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Optional tenant passthrough for federated setups",
"name": "tenant",
"in": "query"
},
{
"type": "string",
"description": "Bearer token to link provider to current account",
"name": "Authorization",
"in": "header"
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/main.SessionWToken"
}
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"410": {
"description": "Login token expired or already used",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/oidc/logged/{provider}": {
"get": {
"description": "Callback endpoint called by OIDC providers after login.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC logged callback",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
},
{
"type": "string",
"description": "State value returned by the provider",
"name": "state",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Authorization code",
"name": "code",
"in": "query"
},
{
"type": "string",
"description": "Provider callback error",
"name": "error",
"in": "query"
}
],
"responses": {
"302": {
"description": "Found"
},
"400": {
"description": "Invalid state",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/oidc/login/{provider}": {
"get": {
"description": "Start an OIDC login with a provider.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC login",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
},
{
"type": "string",
"description": "URL to redirect the browser to after provider callback",
"name": "redirectUrl",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Optional tenant passthrough for federated setups",
"name": "tenant",
"in": "query"
}
],
"responses": {
"302": {
"description": "Found"
},
"400": {
"description": "Missing redirectUrl",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"delete": {
"security": [
{
"Jwt": []
}
],
"description": "Remove an OIDC provider from the current account.",
"produces": [
"application/json"
],
"tags": [
"oidc"
],
"summary": "OIDC unlink provider",
"parameters": [
{
"type": "string",
"example": "google",
"description": "OIDC provider id",
"name": "provider",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Unknown OIDC provider",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/sessions": {
"get": {
"security": [
{
"Jwt": []
}
],
"description": "List all active sessions for the currently connected user",
"produces": [
"application/json"
],
"tags": [
"sessions"
],
"summary": "List my sessions",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/main.SessionWCurrent"
}
}
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"post": {
"description": "Login to your account and open a session",
"consumes": [
@ -368,7 +633,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.Page-main_User"
"$ref": "#/definitions/main.Page-models_User"
}
},
"422": {
@ -404,7 +669,7 @@
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.RegisterDto"
"$ref": "#/definitions/models.RegisterDto"
}
}
],
@ -415,6 +680,12 @@
"$ref": "#/definitions/main.SessionWToken"
}
},
"403": {
"description": "Registrations are disabled",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"409": {
"description": "Duplicated email or username",
"schema": {
@ -449,7 +720,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"401": {
@ -487,7 +758,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
}
}
@ -515,7 +786,7 @@
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
"$ref": "#/definitions/models.EditUserDto"
}
}
],
@ -523,7 +794,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"403": {
@ -541,6 +812,137 @@
}
}
},
"/users/me/logo": {
"get": {
"security": [
{
"Jwt": []
}
],
"description": "Get the current user's logo (manual upload if available, gravatar otherwise)",
"produces": [
"image/*"
],
"tags": [
"users"
],
"summary": "Get my logo",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "No gravatar image found for this user",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"post": {
"security": [
{
"Jwt": []
}
],
"description": "Upload a manual profile picture for the current user",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Upload my logo",
"parameters": [
{
"type": "file",
"description": "Profile picture image (jpeg/png/gif/webp, max 5MB)",
"name": "logo",
"in": "formData",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"413": {
"description": "File too large",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"422": {
"description": "Missing or invalid logo file",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
},
"delete": {
"security": [
{
"Jwt": []
}
],
"description": "Delete the current user's manually uploaded profile picture",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Delete my logo",
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/me/password": {
"patch": {
"security": [
@ -572,7 +974,7 @@
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditPasswordDto"
"$ref": "#/definitions/models.EditPasswordDto"
}
}
],
@ -589,6 +991,49 @@
}
}
},
"/users/me/{id}": {
"delete": {
"security": [
{
"Jwt": []
}
],
"description": "Delete the user's manually uploaded profile picture",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Delete user logo",
"parameters": [
{
"type": "string",
"description": "The id or username of the user",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Invalid jwt token (or expired)",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/{id}": {
"get": {
"security": [
@ -620,7 +1065,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"404": {
@ -669,7 +1114,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"404": {
@ -718,7 +1163,7 @@
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
"$ref": "#/definitions/models.EditUserDto"
}
}
],
@ -726,7 +1171,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"403": {
@ -743,6 +1188,104 @@
}
}
}
},
"/users/{id}/logo": {
"get": {
"security": [
{
"Jwt": [
"users.read"
]
}
],
"description": "Get a user's logo (manual upload if available, gravatar otherwise)",
"produces": [
"image/*"
],
"tags": [
"users"
],
"summary": "Get user logo",
"parameters": [
{
"type": "string",
"description": "The id or username of the user",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "No gravatar image found for this user",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/{id}/sessions": {
"get": {
"security": [
{
"Jwt": []
}
],
"description": "List all active sessions for a user. Listing someone else's sessions requires users.read.",
"produces": [
"application/json"
],
"tags": [
"sessions"
],
"summary": "List user sessions",
"parameters": [
{
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397",
"description": "The id or username of the user",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/main.Session"
}
}
},
"401": {
"description": "Missing jwt token",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"403": {
"description": "Missing permissions: users.read.",
"schema": {
"$ref": "#/definitions/main.KError"
}
},
"404": {
"description": "No user found with id or username",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
}
},
"definitions": {
@ -828,40 +1371,6 @@
}
}
},
"main.EditPasswordDto": {
"type": "object",
"required": [
"password"
],
"properties": {
"password": {
"type": "string",
"example": "password1234"
}
}
},
"main.EditUserDto": {
"type": "object",
"properties": {
"claims": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"preferOriginal": " true"
}
},
"email": {
"type": "string",
"example": "kyoo@zoriya.dev"
},
"username": {
"type": "string",
"example": "zoriya"
}
}
},
"main.JwkSet": {
"type": "object",
"properties": {
@ -943,24 +1452,14 @@
}
}
},
"main.OidcHandle": {
"main.OidcInfo": {
"type": "object",
"properties": {
"id": {
"description": "Id of this oidc handle.",
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
"logo": {
"type": "string"
},
"profileUrl": {
"description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.",
"type": "string",
"format": "url",
"example": "https://myanimelist.net/profile/zoriya"
},
"username": {
"description": "Username of the user on the external service.",
"type": "string",
"example": "zoriya"
"name": {
"type": "string"
}
}
},
@ -983,13 +1482,13 @@
}
}
},
"main.Page-main_User": {
"main.Page-models_User": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/main.User"
"$ref": "#/definitions/models.User"
}
},
"next": {
@ -1002,29 +1501,20 @@
}
}
},
"main.RegisterDto": {
"main.ServerInfo": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"description": "Valid email that could be used for forgotten password requests. Can be used for login.",
"type": "string",
"format": "email",
"example": "kyoo@zoriya.dev"
"allowRegister": {
"type": "boolean"
},
"password": {
"description": "Password to use.",
"type": "string",
"example": "password1234"
"oidc": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/main.OidcInfo"
}
},
"username": {
"description": "Username of the new account, can't contain @ signs. Can be used for login.",
"type": "string",
"example": "zoriya"
"publicUrl": {
"type": "string"
}
}
},
@ -1053,6 +1543,34 @@
}
}
},
"main.SessionWCurrent": {
"type": "object",
"properties": {
"createdDate": {
"description": "When was the session first opened",
"type": "string",
"example": "2025-03-29T18:20:05.267Z"
},
"current": {
"type": "boolean"
},
"device": {
"description": "Device that created the session.",
"type": "string",
"example": "Web - Firefox"
},
"id": {
"description": "Unique id of this session. Can be used for calls to DELETE",
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
},
"lastUsed": {
"description": "Last date this session was used to access a service.",
"type": "string",
"example": "2025-03-29T18:20:05.267Z"
}
}
},
"main.SessionWToken": {
"type": "object",
"properties": {
@ -1082,7 +1600,92 @@
}
}
},
"main.User": {
"models.EditPasswordDto": {
"type": "object",
"required": [
"newPassword"
],
"properties": {
"newPassword": {
"type": "string",
"example": "password1234"
},
"oldPassword": {
"type": "string",
"example": "password1234"
}
}
},
"models.EditUserDto": {
"type": "object",
"properties": {
"claims": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"preferOriginal": " true"
}
},
"email": {
"type": "string",
"example": "kyoo@zoriya.dev"
},
"username": {
"type": "string",
"example": "zoriya"
}
}
},
"models.OidcHandle": {
"type": "object",
"properties": {
"id": {
"description": "Id of this oidc handle.",
"type": "string",
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
},
"profileUrl": {
"description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.",
"type": "string",
"format": "url",
"example": "https://myanimelist.net/profile/zoriya"
},
"username": {
"description": "Username of the user on the external service.",
"type": "string",
"example": "zoriya"
}
}
},
"models.RegisterDto": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"description": "Valid email that could be used for forgotten password requests. Can be used for login.",
"type": "string",
"format": "email",
"example": "kyoo@zoriya.dev"
},
"password": {
"description": "Password to use.",
"type": "string",
"example": "password1234"
},
"username": {
"description": "Username of the new account, can't contain @ signs. Can be used for login.",
"type": "string",
"example": "zoriya"
}
}
},
"models.User": {
"type": "object",
"properties": {
"claims": {
@ -1106,6 +1709,10 @@
"format": "email",
"example": "kyoo@zoriya.dev"
},
"hasPassword": {
"description": "False if the user has never setup a password and only used oidc.",
"type": "boolean"
},
"id": {
"description": "Id of the user.",
"type": "string",
@ -1120,7 +1727,7 @@
"description": "List of other login method available for this user. Access tokens wont be returned here.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/main.OidcHandle"
"$ref": "#/definitions/models.OidcHandle"
}
},
"username": {

1164
auth/docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,16 @@ require (
github.com/exaring/otelpgx v0.10.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.8.0
github.com/jackc/pgx/v5 v5.9.1
github.com/labstack/echo-jwt/v5 v5.0.1
github.com/labstack/echo-opentelemetry v0.0.2
github.com/labstack/echo/v5 v5.0.4
github.com/lestrrat-go/jwx/v3 v3.0.13
github.com/swaggo/echo-swagger v1.5.0
github.com/mileusna/useragent v1.3.5
github.com/swaggo/echo-swagger/v2 v2.0.1
github.com/swaggo/swag v1.16.6
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0
@ -31,47 +32,47 @@ require (
require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/sv-tools/openapi v0.2.1 // indirect
github.com/swaggo/swag/v2 v2.0.0-rc4 // indirect
github.com/sv-tools/openapi v0.4.0 // indirect
github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
golang.org/x/mod v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1
@ -81,17 +82,15 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mileusna/useragent v1.3.5
github.com/swaggo/files/v2 v2.0.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
)

View File

@ -15,11 +15,10 @@ github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@ -34,40 +33,40 @@ github.com/exaring/otelpgx v0.10.0 h1:NGGegdoBQM3jNZDKG8ENhigUcgBN7d7943L0YlcIpZ
github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -76,8 +75,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@ -98,8 +97,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -122,8 +121,8 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs=
github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA=
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
@ -157,16 +156,16 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA=
github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg=
github.com/swaggo/echo-swagger v1.5.0 h1:nkHxOaBy0SkbJMtMeXZC64KHSa0mJdZFQhVqwEcMres=
github.com/swaggo/echo-swagger v1.5.0/go.mod h1:TzO363X1ZG/MSbjrG2IX6m65Yd3/zpqh5KM6lPctAhk=
github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4=
github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE=
github.com/swaggo/echo-swagger/v2 v2.0.1 h1:jKR3QiK+ciGjxE0+7qZ/azjtlx/pTVls7pJFJqdJoJI=
github.com/swaggo/echo-swagger/v2 v2.0.1/go.mod h1:BbgiO9XKX6yYU5Rq4ejqVlQI0mVRv6ziFKd0XgdztnQ=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk=
github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -174,8 +173,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 h1:NFIS6x7wyObQ7cR84x7bt1sr8nYBx89s3x3GwRjw40k=
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0/go.mod h1:39SaByOyDMRMe872AE7uelMuQZidIw7LLFAnQi0FWTE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
@ -208,33 +207,35 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -243,8 +244,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -256,23 +257,23 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@ -280,10 +281,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@ -25,7 +25,7 @@ import (
echojwt "github.com/labstack/echo-jwt/v5"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
echoSwagger "github.com/swaggo/echo-swagger/v2"
"github.com/exaring/otelpgx"
)

View File

@ -544,7 +544,7 @@ type OidcInfo struct {
// @Description List keibi's settings (oidc providers, public url...)
// @Tags oidc
// @Produce json
// @Success 200 ServerInfo
// @Success 200 {object} ServerInfo
// @Router /info [get]
func (h *Handler) Info(c *echo.Context) error {
ret := ServerInfo{

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Role } from "~/models";
import { Container, H2, Image, P, Poster, Skeleton, SubP } from "~/primitives";
import { Container, H2, P, Poster, Skeleton, SubP } from "~/primitives";
import { InfiniteGrid, type QueryIdentifier } from "~/query";
import { EmptyView } from "../empty-view";

View File

@ -176,6 +176,7 @@ export const QualityMenu = ({
}}
/>
{lvls
.reverse()
.map((x) => (
<Menu.Item
key={x.id}

View File

@ -35,10 +35,7 @@ Projects using gocoder:
I did a blog post explaining the core idea, you can find it at [zoriya.dev/blogs/transcoder](https://zoriya.dev/blogs/transcoder).
## TODO:
- Add a swagger
- Add configurable JWT authorization (v5 of kyoo)
- Add credits/recaps/intro/preview detection
- Add multiples qualities for audio streams
- Improve multi-video support
- Add optional redis synchronization for replication (`RunLock` was made with this in mind)
- fmp4 support

View File

@ -20,9 +20,9 @@ func RegisterStreamHandlers(e *echo.Group, transcoder *src.Transcoder) {
e.GET("/:path/direct/:identifier", DirectStream)
e.GET("/:path/master.m3u8", h.GetMaster)
e.GET("/:path/:video/:quality/index.m3u8", h.GetVideoIndex)
e.GET("/:path/audio/:audio/index.m3u8", h.GetAudioIndex)
e.GET("/:path/audio/:audio/:quality/index.m3u8", h.GetAudioIndex)
e.GET("/:path/:video/:quality/:chunk", h.GetVideoSegment)
e.GET("/:path/audio/:audio/:chunk", h.GetAudioSegment)
e.GET("/:path/audio/:audio/:quality/:chunk", h.GetAudioSegment)
}
// @Summary Direct video
@ -88,7 +88,7 @@ func (h *shandler) GetVideoIndex(c echo.Context) error {
if err != nil {
return err
}
quality, err := src.QualityFromString(c.Param("quality"))
quality, err := src.VideoQualityFromString(c.Param("quality"))
if err != nil {
return err
}
@ -115,7 +115,7 @@ func (h *shandler) GetVideoIndex(c echo.Context) error {
// This route can take a few seconds to respond since it will way for at least one segment to be
// available.
//
// Path: /:path/audio/:audio/index.m3u8
// Path: /:path/audio/:audio/:quality/index.m3u8
//
// PRIVATE ROUTE (not documented in swagger, can change at any time)
// Only reached via the master.m3u8.
@ -124,6 +124,10 @@ func (h *shandler) GetAudioIndex(c echo.Context) error {
if err != nil {
return err
}
quality, err := src.AudioQualityFromString(c.Param("quality"))
if err != nil {
return err
}
client, err := getClientId(c)
if err != nil {
return err
@ -133,7 +137,7 @@ func (h *shandler) GetAudioIndex(c echo.Context) error {
return err
}
ret, err := h.transcoder.GetAudioIndex(c.Request().Context(), path, uint32(audio), client, sha)
ret, err := h.transcoder.GetAudioIndex(c.Request().Context(), path, uint32(audio), quality, client, sha)
if err != nil {
return err
}
@ -154,7 +158,7 @@ func (h *shandler) GetVideoSegment(c echo.Context) error {
if err != nil {
return err
}
quality, err := src.QualityFromString(c.Param("quality"))
quality, err := src.VideoQualityFromString(c.Param("quality"))
if err != nil {
return err
}
@ -190,7 +194,7 @@ func (h *shandler) GetVideoSegment(c echo.Context) error {
//
// Retrieve a chunk of a transcoded audio.
//
// Path: /:path/audio/:audio/segments-:chunk.ts
// Path: /:path/audio/:audio/:quality/segments-:chunk.ts
//
// PRIVATE ROUTE (not documented in swagger, can change at any time)
// Only reached via the master.m3u8.
@ -199,6 +203,10 @@ func (h *shandler) GetAudioSegment(c echo.Context) error {
if err != nil {
return err
}
quality, err := src.AudioQualityFromString(c.Param("quality"))
if err != nil {
return err
}
segment, err := parseSegment(c.Param("chunk"))
if err != nil {
return err
@ -212,7 +220,7 @@ func (h *shandler) GetAudioSegment(c echo.Context) error {
return err
}
ret, err := h.transcoder.GetAudioSegment(c.Request().Context(), path, uint32(audio), segment, client, sha)
ret, err := h.transcoder.GetAudioSegment(c.Request().Context(), path, uint32(audio), quality, segment, client, sha)
if err != nil {
return err
}

View File

@ -0,0 +1,60 @@
package src
import (
"net/http"
"github.com/labstack/echo/v4"
)
type AudioQuality string
const (
K128 AudioQuality = "128k"
K192 AudioQuality = "192k"
K256 AudioQuality = "256k"
K320 AudioQuality = "320k"
K512 AudioQuality = "512k"
AOriginal AudioQuality = "original"
)
var AudioQualities = []AudioQuality{K128, K192, K256, K320, K512}
func AudioQualityFromString(str string) (AudioQuality, error) {
if str == string(AOriginal) {
return AOriginal, nil
}
for _, quality := range AudioQualities {
if string(quality) == str {
return quality, nil
}
}
return AOriginal, echo.NewHTTPError(http.StatusBadRequest, "Invalid quality")
}
func (a AudioQuality) Bitrate() uint32 {
switch a {
case K128:
return 128_000
case K192:
return 192_000
case K256:
return 256_000
case K320:
return 320_000
case K512:
return 512_000
case AOriginal:
panic("Original quality must be handled specially")
}
panic("Invalid quality value")
}
func (audio *Audio) Quality() AudioQuality {
for _, quality := range AudioQualities {
if quality.Bitrate() >= audio.Bitrate {
return quality
}
}
return K128
}

View File

@ -7,10 +7,11 @@ import (
type AudioStream struct {
Stream
index uint32
audio *Audio
quality AudioQuality
}
func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32) (*AudioStream, error) {
func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32, quality AudioQuality) (*AudioStream, error) {
log.Printf("Creating a audio stream %d for %s", idx, file.Info.Path)
keyframes, err := t.metadataService.GetKeyframes(file.Info, false, idx)
@ -19,13 +20,20 @@ func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32) (*AudioStream,
}
ret := new(AudioStream)
ret.index = idx
ret.quality = quality
for _, audio := range file.Info.Audios {
if audio.Index == idx {
ret.audio = &audio
break
}
}
NewStream(file, keyframes, ret, &ret.Stream)
return ret, nil
}
func (as *AudioStream) getOutPath(encoder_id int) string {
return fmt.Sprintf("%s/segment-a%d-%d-%%d.ts", as.file.Out, as.index, encoder_id)
return fmt.Sprintf("%s/segment-a%d-%s-%d-%%d.ts", as.file.Out, as.audio.Index, string(as.quality), encoder_id)
}
func (as *AudioStream) getFlags() Flags {
@ -33,12 +41,18 @@ func (as *AudioStream) getFlags() Flags {
}
func (as *AudioStream) getTranscodeArgs(segments string) []string {
return []string{
"-map", fmt.Sprintf("0:a:%d", as.index),
"-c:a", "aac",
// TODO: Support 5.1 audio streams.
"-ac", "2",
// TODO: Support multi audio qualities.
"-b:a", "128k",
args := []string{
"-map", fmt.Sprintf("0:a:%d", as.audio.Index),
}
if as.quality == AOriginal {
args = append(args, "-c:a", "copy")
} else {
args = append(args,
// TODO: Support 5.1 audio streams.
"-ac", "2",
"-b:a", fmt.Sprint(as.quality.Bitrate()),
"-c:a", "aac",
)
}
return args
}

View File

@ -6,6 +6,7 @@ import (
"log"
"math"
"os"
"slices"
"strings"
"sync"
@ -19,12 +20,17 @@ type FileStream struct {
Out string
Info *MediaInfo
videos CMap[VideoKey, *VideoStream]
audios CMap[uint32, *AudioStream]
audios CMap[AudioKey, *AudioStream]
}
type AudioKey struct {
idx uint32
quality AudioQuality
}
type VideoKey struct {
idx uint32
quality Quality
quality VideoQuality
}
func (t *Transcoder) newFileStream(path string, sha string) *FileStream {
@ -32,7 +38,7 @@ func (t *Transcoder) newFileStream(path string, sha string) *FileStream {
transcoder: t,
Out: fmt.Sprintf("%s/%s", Settings.Outpath, sha),
videos: NewCMap[VideoKey, *VideoStream](),
audios: NewCMap[uint32, *AudioStream](),
audios: NewCMap[AudioKey, *AudioStream](),
}
ret.ready.Add(1)
@ -71,10 +77,28 @@ func (fs *FileStream) Destroy() {
func (fs *FileStream) GetMaster(client string) string {
master := "#EXTM3U\n"
// TODO: support multiples audio qualities (and original)
for _, audio := range fs.Info.Audios {
for _, quality := range AudioQualities {
master += "#EXT-X-MEDIA:TYPE=AUDIO,"
master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", quality)
if audio.Language != nil {
master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language)
}
if audio.Title != nil {
master += fmt.Sprintf("NAME=\"%s\",", *audio.Title)
} else if audio.Language != nil {
master += fmt.Sprintf("NAME=\"%s\",", *audio.Language)
} else {
master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index)
}
if audio.IsDefault {
master += "DEFAULT=YES,"
}
master += "CHANNELS=\"2\","
master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, quality, client)
}
master += "#EXT-X-MEDIA:TYPE=AUDIO,"
master += "GROUP-ID=\"audio\","
master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", AOriginal)
if audio.Language != nil {
master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language)
}
@ -88,17 +112,17 @@ func (fs *FileStream) GetMaster(client string) string {
if audio.IsDefault {
master += "DEFAULT=YES,"
}
master += "CHANNELS=\"2\","
master += fmt.Sprintf("URI=\"audio/%d/index.m3u8?clientId=%s\"\n", audio.Index, client)
master += "CHANNELS=\"2\"," // TODO
master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, AOriginal, client)
}
master += "\n"
// codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow
transcode_prefix := "avc1.6400"
transcode_codec := transcode_prefix + "28"
audio_codec := "mp4a.40.2"
transcode_audio_codec := "mp4a.40.2"
var def_video *Video
var def_audio *Audio
for _, video := range fs.Info.Videos {
if video.IsDefault {
def_video = &video
@ -108,12 +132,14 @@ func (fs *FileStream) GetMaster(client string) string {
if def_video == nil && len(fs.Info.Videos) > 0 {
def_video = &fs.Info.Videos[0]
}
if len(fs.Info.Audios) > 0 {
def_audio = &fs.Info.Audios[0]
}
if def_video != nil {
qualities := utils.Filter(Qualities, func(quality Quality) bool {
qualities := utils.Filter(VideoQualities, func(quality VideoQuality) bool {
return quality.Height() < def_video.Height
})
transcode_count := len(qualities)
// NoResize is the same idea as Original but we change the codec.
// This is only needed when the original's codec is different from what we would transcode it to.
@ -146,22 +172,32 @@ func (fs *FileStream) GetMaster(client string) string {
master += "\n"
aspectRatio := float32(def_video.Width) / float32(def_video.Height)
for i, quality := range qualities {
if i >= transcode_count {
// original & noresize streams
bitrate := float64(def_video.Bitrate)
master += "#EXT-X-STREAM-INF:"
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(def_video.Quality().AverageBitrate()))))
master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(def_video.Quality().MaxBitrate()))))
master += fmt.Sprintf("RESOLUTION=%dx%d,", def_video.Width, def_video.Height)
if quality != Original {
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ","))
} else if def_video.MimeCodec != nil {
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ","))
for _, quality := range slices.Backward(qualities) {
if quality == Original || quality == NoResize {
audios := []AudioQuality{AOriginal}
if def_audio != nil && (def_audio.MimeCodec == nil || *def_audio.MimeCodec != transcode_audio_codec) {
audios = append(audios, matchAudioQuality(def_video.Quality()))
}
for _, audio_quality := range audios {
// original & noresize streams
bitrate := float64(def_video.Bitrate)
master += "#EXT-X-STREAM-INF:"
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(def_video.Quality().AverageBitrate()))))
master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(def_video.Quality().MaxBitrate()))))
master += fmt.Sprintf("RESOLUTION=%dx%d,", def_video.Width, def_video.Height)
var audio_codec = transcode_audio_codec
if def_audio != nil && audio_quality == AOriginal {
audio_codec = *def_audio.MimeCodec
}
if quality != Original {
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ","))
} else if def_video.MimeCodec != nil {
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ","))
}
master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(audio_quality))
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client)
}
master += "AUDIO=\"audio\","
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client)
continue
}
@ -169,8 +205,8 @@ func (fs *FileStream) GetMaster(client string) string {
master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate())
master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate())
master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height())
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ","))
master += "AUDIO=\"audio\","
master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, transcode_audio_codec}, ","))
master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(matchAudioQuality(quality)))
master += "CLOSED-CAPTIONS=NONE\n"
master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client)
}
@ -179,7 +215,7 @@ func (fs *FileStream) GetMaster(client string) string {
return master
}
func (fs *FileStream) getVideoStream(idx uint32, quality Quality) (*VideoStream, error) {
func (fs *FileStream) getVideoStream(idx uint32, quality VideoQuality) (*VideoStream, error) {
stream, _ := fs.videos.GetOrCreate(VideoKey{idx, quality}, func() *VideoStream {
ret, _ := fs.transcoder.NewVideoStream(fs, idx, quality)
return ret
@ -188,7 +224,7 @@ func (fs *FileStream) getVideoStream(idx uint32, quality Quality) (*VideoStream,
return stream, nil
}
func (fs *FileStream) GetVideoIndex(idx uint32, quality Quality, client string) (string, error) {
func (fs *FileStream) GetVideoIndex(idx uint32, quality VideoQuality, client string) (string, error) {
stream, err := fs.getVideoStream(idx, quality)
if err != nil {
return "", err
@ -196,7 +232,7 @@ func (fs *FileStream) GetVideoIndex(idx uint32, quality Quality, client string)
return stream.GetIndex(client)
}
func (fs *FileStream) GetVideoSegment(idx uint32, quality Quality, segment int32) (string, error) {
func (fs *FileStream) GetVideoSegment(idx uint32, quality VideoQuality, segment int32) (string, error) {
stream, err := fs.getVideoStream(idx, quality)
if err != nil {
return "", err
@ -204,27 +240,50 @@ func (fs *FileStream) GetVideoSegment(idx uint32, quality Quality, segment int32
return stream.GetSegment(segment)
}
func (fs *FileStream) getAudioStream(audio uint32) (*AudioStream, error) {
stream, _ := fs.audios.GetOrCreate(audio, func() *AudioStream {
ret, _ := fs.transcoder.NewAudioStream(fs, audio)
func (fs *FileStream) getAudioStream(idx uint32, quality AudioQuality) (*AudioStream, error) {
stream, _ := fs.audios.GetOrCreate(AudioKey{idx, quality}, func() *AudioStream {
ret, _ := fs.transcoder.NewAudioStream(fs, idx, quality)
return ret
})
stream.ready.Wait()
return stream, nil
}
func (fs *FileStream) GetAudioIndex(audio uint32, client string) (string, error) {
stream, err := fs.getAudioStream(audio)
func (fs *FileStream) GetAudioIndex(idx uint32, quality AudioQuality, client string) (string, error) {
stream, err := fs.getAudioStream(idx, quality)
if err != nil {
return "", nil
}
return stream.GetIndex(client)
}
func (fs *FileStream) GetAudioSegment(audio uint32, segment int32) (string, error) {
stream, err := fs.getAudioStream(audio)
func (fs *FileStream) GetAudioSegment(idx uint32, quality AudioQuality, segment int32) (string, error) {
stream, err := fs.getAudioStream(idx, quality)
if err != nil {
return "", nil
}
return stream.GetSegment(segment)
}
func matchAudioQuality(q VideoQuality) AudioQuality {
switch q {
case P240:
return K128
case P360:
return K128
case P480:
return K128
case P720:
return K192
case P1080:
return K192
case P1440:
return K256
case P4k:
return K512
case P8k:
return K512
default:
return AOriginal
}
}

View File

@ -10,7 +10,7 @@ type ClientInfo struct {
sha string
path string
video *VideoKey
audio *uint32
audio *AudioKey
vhead int32
ahead int32
}
@ -151,7 +151,7 @@ func (t *Tracker) DestroyStreamIfOld(sha string) {
stream.Destroy()
}
func (t *Tracker) KillAudioIfDead(sha string, path string, audio uint32) bool {
func (t *Tracker) KillAudioIfDead(sha string, path string, audio AudioKey) bool {
for _, stream := range t.clients {
if stream.sha == sha && stream.audio != nil && *stream.audio == audio {
return false
@ -191,7 +191,7 @@ func (t *Tracker) KillVideoIfDead(sha string, path string, video VideoKey) bool
return true
}
func (t *Tracker) KillOrphanedHeads(sha string, video *VideoKey, audio *uint32) {
func (t *Tracker) KillOrphanedHeads(sha string, video *VideoKey, audio *AudioKey) {
stream, ok := t.transcoder.streams.Get(sha)
if !ok {
return

View File

@ -70,7 +70,7 @@ func (t *Transcoder) GetVideoIndex(
ctx context.Context,
path string,
video uint32,
quality Quality,
quality VideoQuality,
client string,
sha string,
) (string, error) {
@ -94,6 +94,7 @@ func (t *Transcoder) GetAudioIndex(
ctx context.Context,
path string,
audio uint32,
quality AudioQuality,
client string,
sha string,
) (string, error) {
@ -105,18 +106,18 @@ func (t *Transcoder) GetAudioIndex(
client: client,
sha: sha,
path: path,
audio: &audio,
audio: &AudioKey{audio, quality},
vhead: -1,
ahead: -1,
}
return stream.GetAudioIndex(audio, client)
return stream.GetAudioIndex(audio, quality, client)
}
func (t *Transcoder) GetVideoSegment(
ctx context.Context,
path string,
video uint32,
quality Quality,
quality VideoQuality,
segment int32,
client string,
sha string,
@ -141,6 +142,7 @@ func (t *Transcoder) GetAudioSegment(
ctx context.Context,
path string,
audio uint32,
quality AudioQuality,
segment int32,
client string,
sha string,
@ -153,9 +155,9 @@ func (t *Transcoder) GetAudioSegment(
client: client,
sha: sha,
path: path,
audio: &audio,
audio: &AudioKey{audio, quality},
ahead: segment,
vhead: -1,
}
return stream.GetAudioSegment(audio, segment)
return stream.GetAudioSegment(audio, quality, segment)
}

View File

@ -6,25 +6,25 @@ import (
"github.com/labstack/echo/v4"
)
type Quality string
type VideoQuality string
const (
P240 Quality = "240p"
P360 Quality = "360p"
P480 Quality = "480p"
P720 Quality = "720p"
P1080 Quality = "1080p"
P1440 Quality = "1440p"
P4k Quality = "4k"
P8k Quality = "8k"
NoResize Quality = "transcode"
Original Quality = "original"
P240 VideoQuality = "240p"
P360 VideoQuality = "360p"
P480 VideoQuality = "480p"
P720 VideoQuality = "720p"
P1080 VideoQuality = "1080p"
P1440 VideoQuality = "1440p"
P4k VideoQuality = "4k"
P8k VideoQuality = "8k"
NoResize VideoQuality = "transcode"
Original VideoQuality = "original"
)
// Purposfully removing Original from this list (since it require special treatments anyways)
var Qualities = []Quality{P240, P360, P480, P720, P1080, P1440, P4k, P8k}
var VideoQualities = []VideoQuality{P240, P360, P480, P720, P1080, P1440, P4k, P8k}
func QualityFromString(str string) (Quality, error) {
func VideoQualityFromString(str string) (VideoQuality, error) {
if str == string(Original) {
return Original, nil
}
@ -32,7 +32,7 @@ func QualityFromString(str string) (Quality, error) {
return NoResize, nil
}
for _, quality := range Qualities {
for _, quality := range VideoQualities {
if string(quality) == str {
return quality, nil
}
@ -41,7 +41,7 @@ func QualityFromString(str string) (Quality, error) {
}
// I'm not entierly sure about the values for bitrates. Double checking would be nice.
func (v Quality) AverageBitrate() uint32 {
func (v VideoQuality) AverageBitrate() uint32 {
switch v {
case P240:
return 400_000
@ -65,7 +65,7 @@ func (v Quality) AverageBitrate() uint32 {
panic("Invalid quality value")
}
func (v Quality) MaxBitrate() uint32 {
func (v VideoQuality) MaxBitrate() uint32 {
switch v {
case P240:
return 700_000
@ -89,7 +89,7 @@ func (v Quality) MaxBitrate() uint32 {
panic("Invalid quality value")
}
func (q Quality) Height() uint32 {
func (q VideoQuality) Height() uint32 {
switch q {
case P240:
return 240
@ -113,8 +113,8 @@ func (q Quality) Height() uint32 {
panic("Invalid quality value")
}
func (video *Video) Quality() Quality {
for _, quality := range Qualities {
func (video *Video) Quality() VideoQuality {
for _, quality := range VideoQualities {
if quality.Height() >= video.Height || quality.AverageBitrate() >= video.Bitrate {
return quality
}

View File

@ -8,10 +8,10 @@ import (
type VideoStream struct {
Stream
video *Video
quality Quality
quality VideoQuality
}
func (t *Transcoder) NewVideoStream(file *FileStream, idx uint32, quality Quality) (*VideoStream, error) {
func (t *Transcoder) NewVideoStream(file *FileStream, idx uint32, quality VideoQuality) (*VideoStream, error) {
log.Printf(
"Creating a new video stream for %s (n %d) in quality %s",
file.Info.Path,
@ -84,7 +84,7 @@ func (vs *VideoStream) getTranscodeArgs(segments string) []string {
args = append(args, "-vf", Settings.HwAccel.NoResizeFilter)
// NoResize doesn't have bitrate info, fallback to a know quality higher or equal.
for _, q := range Qualities {
for _, q := range VideoQualities {
if q.Height() >= vs.video.Height {
quality = q
break