Compare commits

..

No commits in common. "02f7685eaa7159324afaff05e42b02d2172a8dbc" and "0c94930bca1cbf63623291b43acde17a5a10579e" have entirely different histories.

9 changed files with 381 additions and 809 deletions

View file

@ -146,17 +146,11 @@ paths:
summary: Get user titles summary: Get user titles
parameters: parameters:
- $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/cursor'
- $ref: '#/components/parameters/title_sort'
- in: path - in: path
name: user_id name: user_id
required: true required: true
schema: schema:
type: string type: string
- in: query
name: sort_forward
schema:
type: boolean
default: true
- in: query - in: query
name: word name: word
schema: schema:
@ -164,12 +158,7 @@ paths:
- in: query - in: query
name: status name: status
schema: schema:
type: array $ref: '#/components/schemas/TitleStatus'
items:
$ref: '#/components/schemas/TitleStatus'
description: List of title statuses to filter
style: form
explode: false
- in: query - in: query
name: watch_status name: watch_status
schema: schema:
@ -179,11 +168,6 @@ paths:
schema: schema:
type: number type: number
format: double format: double
- in: query
name: my_rate
schema:
type: integer
format: int32
- in: query - in: query
name: release_year name: release_year
schema: schema:
@ -210,17 +194,9 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object type: array
properties: items:
data: $ref: '#/components/schemas/UserTitle'
type: array
items:
$ref: '#/components/schemas/UserTitle'
cursor:
$ref: '#/components/schemas/CursorObj'
required:
- data
- cursor
'204': '204':
description: No titles found description: No titles found
'400': '400':
@ -448,8 +424,9 @@ components:
user_id: user_id:
type: integer type: integer
format: int64 format: int64
title: title_id:
$ref: '#/components/schemas/Title' type: integer
format: int64
status: status:
$ref: '#/components/schemas/UserTitleStatus' $ref: '#/components/schemas/UserTitleStatus'
rate: rate:

View file

@ -143,7 +143,7 @@ type UserTitle struct {
// Status User's title status // Status User's title status
Status UserTitleStatus `json:"status"` Status UserTitleStatus `json:"status"`
Title *Title `json:"title,omitempty"` TitleId int64 `json:"title_id"`
UserId int64 `json:"user_id"` UserId int64 `json:"user_id"`
AdditionalProperties map[string]interface{} `json:"-"` AdditionalProperties map[string]interface{} `json:"-"`
} }
@ -183,16 +183,11 @@ type GetUsersUserIdParams struct {
// GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. // GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles.
type GetUsersUserIdTitlesParams struct { type GetUsersUserIdTitlesParams struct {
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` Word *string `form:"word,omitempty" json:"word,omitempty"`
SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` Status *TitleStatus `form:"status,omitempty" json:"status,omitempty"`
Word *string `form:"word,omitempty" json:"word,omitempty"`
// Status List of title statuses to filter
Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"`
WatchStatus *UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` WatchStatus *UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"`
Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"`
MyRate *int32 `form:"my_rate,omitempty" json:"my_rate,omitempty"`
ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"`
ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"`
Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"`
@ -498,12 +493,12 @@ func (a *UserTitle) UnmarshalJSON(b []byte) error {
delete(object, "status") delete(object, "status")
} }
if raw, found := object["title"]; found { if raw, found := object["title_id"]; found {
err = json.Unmarshal(raw, &a.Title) err = json.Unmarshal(raw, &a.TitleId)
if err != nil { if err != nil {
return fmt.Errorf("error reading 'title': %w", err) return fmt.Errorf("error reading 'title_id': %w", err)
} }
delete(object, "title") delete(object, "title_id")
} }
if raw, found := object["user_id"]; found { if raw, found := object["user_id"]; found {
@ -559,11 +554,9 @@ func (a UserTitle) MarshalJSON() ([]byte, error) {
return nil, fmt.Errorf("error marshaling 'status': %w", err) return nil, fmt.Errorf("error marshaling 'status': %w", err)
} }
if a.Title != nil { object["title_id"], err = json.Marshal(a.TitleId)
object["title"], err = json.Marshal(a.Title) if err != nil {
if err != nil { return nil, fmt.Errorf("error marshaling 'title_id': %w", err)
return nil, fmt.Errorf("error marshaling 'title': %w", err)
}
} }
object["user_id"], err = json.Marshal(a.UserId) object["user_id"], err = json.Marshal(a.UserId)
@ -806,22 +799,6 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) {
return return
} }
// ------------- Optional query parameter "sort" -------------
err = runtime.BindQueryParameter("form", true, false, "sort", c.Request.URL.Query(), &params.Sort)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort: %w", err), http.StatusBadRequest)
return
}
// ------------- Optional query parameter "sort_forward" -------------
err = runtime.BindQueryParameter("form", true, false, "sort_forward", c.Request.URL.Query(), &params.SortForward)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort_forward: %w", err), http.StatusBadRequest)
return
}
// ------------- Optional query parameter "word" ------------- // ------------- Optional query parameter "word" -------------
err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), &params.Word) err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), &params.Word)
@ -832,7 +809,7 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) {
// ------------- Optional query parameter "status" ------------- // ------------- Optional query parameter "status" -------------
err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), &params.Status) err = runtime.BindQueryParameter("form", true, false, "status", c.Request.URL.Query(), &params.Status)
if err != nil { if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest)
return return
@ -854,14 +831,6 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) {
return return
} }
// ------------- Optional query parameter "my_rate" -------------
err = runtime.BindQueryParameter("form", true, false, "my_rate", c.Request.URL.Query(), &params.MyRate)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter my_rate: %w", err), http.StatusBadRequest)
return
}
// ------------- Optional query parameter "release_year" ------------- // ------------- Optional query parameter "release_year" -------------
err = runtime.BindQueryParameter("form", true, false, "release_year", c.Request.URL.Query(), &params.ReleaseYear) err = runtime.BindQueryParameter("form", true, false, "release_year", c.Request.URL.Query(), &params.ReleaseYear)
@ -1084,10 +1053,7 @@ type GetUsersUserIdTitlesResponseObject interface {
VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error
} }
type GetUsersUserIdTitles200JSONResponse struct { type GetUsersUserIdTitles200JSONResponse []UserTitle
Cursor CursorObj `json:"cursor"`
Data []UserTitle `json:"data"`
}
func (response GetUsersUserIdTitles200JSONResponse) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { func (response GetUsersUserIdTitles200JSONResponse) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View file

@ -2,17 +2,11 @@ get:
summary: Get user titles summary: Get user titles
parameters: parameters:
- $ref: '../parameters/cursor.yaml' - $ref: '../parameters/cursor.yaml'
- $ref: "../parameters/title_sort.yaml"
- in: path - in: path
name: user_id name: user_id
required: true required: true
schema: schema:
type: string type: string
- in: query
name: sort_forward
schema:
type: boolean
default: true
- in: query - in: query
name: word name: word
schema: schema:
@ -20,12 +14,7 @@ get:
- in: query - in: query
name: status name: status
schema: schema:
type: array $ref: '../schemas/enums/TitleStatus.yaml'
items:
$ref: '../schemas/enums/TitleStatus.yaml'
description: List of title statuses to filter
style: form
explode: false
- in: query - in: query
name: watch_status name: watch_status
schema: schema:
@ -35,11 +24,6 @@ get:
schema: schema:
type: number type: number
format: double format: double
- in: query
name: my_rate
schema:
type: integer
format: int32
- in: query - in: query
name: release_year name: release_year
schema: schema:
@ -66,17 +50,9 @@ get:
content: content:
application/json: application/json:
schema: schema:
type: object type: array
properties: items:
data: $ref: '../schemas/UserTitle.yaml'
type: array
items:
$ref: '../schemas/UserTitle.yaml'
cursor:
$ref: '../schemas/CursorObj.yaml'
required:
- data
- cursor
'204': '204':
description: No titles found description: No titles found
'400': '400':

View file

@ -7,8 +7,9 @@ properties:
user_id: user_id:
type: integer type: integer
format: int64 format: int64
title: title_id:
$ref: ./Title.yaml type: integer
format: int64
status: status:
$ref: ./enums/UserTitleStatus.yaml $ref: ./enums/UserTitleStatus.yaml
rate: rate:

View file

@ -1,10 +1,6 @@
package handlers package handlers
import ( import (
"context"
"encoding/json"
"fmt"
oapi "nyanimedb/api"
sqlc "nyanimedb/sql" sqlc "nyanimedb/sql"
"strconv" "strconv"
) )
@ -17,83 +13,70 @@ func NewServer(db *sqlc.Queries) Server {
return Server{db: db} return Server{db: db}
} }
func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi.Title, error) { // type Cursor interface {
// ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error)
oapi_title := oapi.Title{ // Values() map[string]interface{}
EpisodesAired: title.EpisodesAired, // // for logs only
EpisodesAll: title.EpisodesAired, // Type() string
// EpisodesLen: &episodes_lens, // }
Id: title.ID,
// Poster: &oapi_image,
Rating: title.Rating,
RatingCount: title.RatingCount,
// ReleaseSeason: &release_season,
ReleaseYear: title.ReleaseYear,
// Studio: &oapi_studio,
// Tags: oapi_tag_names,
// TitleNames: title_names,
// TitleStatus: oapi_status,
// AdditionalProperties:
}
title_names := make(map[string][]string, 0) // type CursorByID struct {
err := json.Unmarshal(title.TitleNames, &title_names) // ID int64
if err != nil { // }
return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err)
}
if title.EpisodesLen != nil && len(title.EpisodesLen) > 0 { // func (c CursorByID) ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error) {
episodes_lens := make(map[string]float64, 0) // var cur CursorByID
err = json.Unmarshal(title.EpisodesLen, &episodes_lens) // if err := json.Unmarshal(data, &cur); err != nil {
if err != nil { // return nil, fmt.Errorf("invalid cursor (id): %w", err)
return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) // }
} // if cur.ID == 0 {
oapi_title.EpisodesLen = &episodes_lens // return nil, errors.New("cursor id must be non-zero")
} // }
// return cur, nil
// }
oapi_tag_names := make(oapi.Tags, 0) // func (c CursorByID) Values() map[string]interface{} {
err = json.Unmarshal(title.TagNames, &oapi_tag_names) // return map[string]interface{}{
if err != nil { // "cursor_id": c.ID,
return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) // "cursor_year": nil,
} // "cursor_rating": nil,
// }
// }
var oapi_studio oapi.Studio // func (c CursorByID) Type() string { return "id" }
if title.StudioName != nil {
oapi_studio.Name = *title.StudioName
}
if title.StudioID != 0 {
oapi_studio.Id = title.StudioID
oapi_studio.Description = title.StudioDesc
if title.StudioIllustID != nil {
oapi_studio.Poster.Id = title.StudioIllustID
oapi_studio.Poster.ImagePath = title.StudioImagePath
oapi_studio.Poster.StorageType = &title.StudioStorageType
}
}
oapi_title.Studio = &oapi_studio
var oapi_image oapi.Image // func NewCursor(sortBy string) (Cursor, error) {
// switch Type(sortBy) {
// case TypeID:
// return CursorByID{}, nil
// case TypeYear:
// return CursorByYear{}, nil
// case TypeRating:
// return CursorByRating{}, nil
// default:
// return nil, fmt.Errorf("unsupported sort_by: %q", sortBy)
// }
// }
if title.PosterID != nil { // decodes a base64-encoded JSON string into a CursorObj
oapi_image.Id = title.PosterID // Returns the parsed CursorObj and an error
oapi_image.ImagePath = title.TitleImagePath // func parseCursor(encoded oapi.Cursor) (*oapi.CursorObj, error) {
oapi_image.StorageType = &title.TitleStorageType
}
var release_season oapi.ReleaseSeason // // Decode base64
if title.ReleaseSeason != nil { // decoded, err := base64.StdEncoding.DecodeString(encoded)
release_season = oapi.ReleaseSeason(*title.ReleaseSeason) // if err != nil {
} // return nil, fmt.Errorf("parseCursor: %v", err)
oapi_title.ReleaseSeason = &release_season // }
oapi_status, err := TitleStatus2oapi(&title.TitleStatus) // // Parse JSON
if err != nil { // var cursor oapi.CursorObj
return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) // if err := json.Unmarshal(decoded, &cursor); err != nil {
} // return nil, fmt.Errorf("parseCursor: %v", err)
oapi_title.TitleStatus = oapi_status // }
return oapi_title, nil // return &cursor, nil
} // }
func parseInt64(s string) (int32, error) { func parseInt64(s string) (int32, error) {
i, err := strconv.ParseInt(s, 10, 64) i, err := strconv.ParseInt(s, 10, 64)

View file

@ -26,25 +26,25 @@ type SqlcStatus struct {
planned string planned string
} }
// func TitleStatus2Sqlc(s *[]oapi.TitleStatus) (*SqlcStatus, error) { func TitleStatus2Sqlc(s *[]oapi.TitleStatus) (*SqlcStatus, error) {
// var sqlc_status SqlcStatus var sqlc_status SqlcStatus
// if s == nil { if s == nil {
// return &sqlc_status, nil return &sqlc_status, nil
// } }
// for _, t := range *s { for _, t := range *s {
// switch t { switch t {
// case oapi.TitleStatusFinished: case oapi.TitleStatusFinished:
// sqlc_status.finished = "finished" sqlc_status.finished = "finished"
// case oapi.TitleStatusOngoing: case oapi.TitleStatusOngoing:
// sqlc_status.ongoing = "ongoing" sqlc_status.ongoing = "ongoing"
// case oapi.TitleStatusPlanned: case oapi.TitleStatusPlanned:
// sqlc_status.planned = "planned" sqlc_status.planned = "planned"
// default: default:
// return nil, fmt.Errorf("unexpected tittle status: %s", t) return nil, fmt.Errorf("unexpected tittle status: %s", t)
// } }
// } }
// return &sqlc_status, nil return &sqlc_status, nil
// } }
func TitleStatus2oapi(s *sqlc.TitleStatusT) (*oapi.TitleStatus, error) { func TitleStatus2oapi(s *sqlc.TitleStatusT) (*oapi.TitleStatus, error) {
if s == nil { if s == nil {
@ -158,6 +158,78 @@ func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) {
return &oapi_studio, nil return &oapi_studio, nil
} }
func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi.Title, error) {
// var oapi_title oapi.Title
title_names := make(map[string][]string, 0)
err := json.Unmarshal(title.TitleNames, &title_names)
if err != nil {
return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err)
}
episodes_lens := make(map[string]float64, 0)
err = json.Unmarshal(title.EpisodesLen, &episodes_lens)
if err != nil {
return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err)
}
oapi_tag_names := make(oapi.Tags, 0)
err = json.Unmarshal(title.TagNames, &oapi_tag_names)
if err != nil {
return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err)
}
var oapi_studio oapi.Studio
oapi_studio.Id = title.StudioID
if title.StudioName != nil {
oapi_studio.Name = *title.StudioName
}
oapi_studio.Description = title.StudioDesc
if title.StudioIllustID != nil {
oapi_studio.Poster.Id = title.StudioIllustID
oapi_studio.Poster.ImagePath = title.StudioImagePath
oapi_studio.Poster.StorageType = &title.StudioStorageType
}
var oapi_image oapi.Image
if title.PosterID != nil {
oapi_image.Id = title.PosterID
oapi_image.ImagePath = title.TitleImagePath
oapi_image.StorageType = &title.TitleStorageType
}
var release_season oapi.ReleaseSeason
if title.ReleaseSeason != nil {
release_season = oapi.ReleaseSeason(*title.ReleaseSeason)
}
oapi_status, err := TitleStatus2oapi(&title.TitleStatus)
if err != nil {
return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err)
}
oapi_title := oapi.Title{
EpisodesAired: title.EpisodesAired,
EpisodesAll: title.EpisodesAired,
EpisodesLen: &episodes_lens,
Id: title.ID,
Poster: &oapi_image,
Rating: title.Rating,
RatingCount: title.RatingCount,
ReleaseSeason: &release_season,
ReleaseYear: title.ReleaseYear,
Studio: &oapi_studio,
Tags: oapi_tag_names,
TitleNames: title_names,
TitleStatus: oapi_status,
// AdditionalProperties:
}
return oapi_title, nil
}
func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitleIdRequestObject) (oapi.GetTitlesTitleIdResponseObject, error) { func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitleIdRequestObject) (oapi.GetTitlesTitleIdResponseObject, error) {
var oapi_title oapi.Title var oapi_title oapi.Title
@ -169,8 +241,29 @@ func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitl
log.Errorf("%v", err) log.Errorf("%v", err)
return oapi.GetTitlesTitleId500Response{}, nil return oapi.GetTitlesTitleId500Response{}, nil
} }
_sqlc_title := sqlc.SearchTitlesRow{
oapi_title, err = s.mapTitle(ctx, sqlc_title) ID: sqlc_title.ID,
StudioID: sqlc_title.StudioID,
PosterID: sqlc_title.PosterID,
TitleStatus: sqlc_title.TitleStatus,
Rating: sqlc_title.Rating,
RatingCount: sqlc_title.RatingCount,
ReleaseYear: sqlc_title.ReleaseYear,
ReleaseSeason: sqlc_title.ReleaseSeason,
Season: sqlc_title.Season,
EpisodesAired: sqlc_title.EpisodesAired,
EpisodesAll: sqlc_title.EpisodesAll,
EpisodesLen: sqlc_title.EpisodesLen,
TitleStorageType: sqlc_title.TitleStorageType,
TitleImagePath: sqlc_title.TitleImagePath,
TagNames: sqlc_title.TitleNames,
StudioName: sqlc_title.StudioName,
StudioIllustID: sqlc_title.StudioIllustID,
StudioDesc: sqlc_title.StudioDesc,
StudioStorageType: sqlc_title.StudioStorageType,
StudioImagePath: sqlc_title.StudioImagePath,
}
oapi_title, err = s.mapTitle(ctx, _sqlc_title)
if err != nil { if err != nil {
log.Errorf("%v", err) log.Errorf("%v", err)
return oapi.GetTitlesTitleId500Response{}, nil return oapi.GetTitlesTitleId500Response{}, nil
@ -183,6 +276,11 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
opai_titles := make([]oapi.Title, 0) opai_titles := make([]oapi.Title, 0)
word := Word2Sqlc(request.Params.Word) word := Word2Sqlc(request.Params.Word)
status, err := TitleStatus2Sqlc(request.Params.Status)
if err != nil {
log.Errorf("%v", err)
return oapi.GetTitles400Response{}, err
}
season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason)
if err != nil { if err != nil {
@ -190,22 +288,16 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
return oapi.GetTitles400Response{}, err return oapi.GetTitles400Response{}, err
} }
var statuses_sort []string
if request.Params.Status != nil {
for _, s := range *request.Params.Status {
ss := string(s) // s type is alias for string
statuses_sort = append(statuses_sort, ss)
}
}
params := sqlc.SearchTitlesParams{ params := sqlc.SearchTitlesParams{
Word: word, Word: word,
TitleStatuses: statuses_sort, Ongoing: status.ongoing,
Finished: status.finished,
Planned: status.planned,
Rating: request.Params.Rating, Rating: request.Params.Rating,
ReleaseYear: request.Params.ReleaseYear, ReleaseYear: request.Params.ReleaseYear,
ReleaseSeason: season, ReleaseSeason: season,
Forward: true, // default Forward: true,
SortBy: "id", // default SortBy: "id",
Limit: request.Params.Limit, Limit: request.Params.Limit,
} }
@ -215,7 +307,6 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
if request.Params.Sort != nil { if request.Params.Sort != nil {
params.SortBy = string(*request.Params.Sort) params.SortBy = string(*request.Params.Sort)
if request.Params.Cursor != nil { if request.Params.Cursor != nil {
// here we set CursorYear CursorID CursorRating fields
err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), &params) err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), &params)
if err != nil { if err != nil {
log.Errorf("%v", err) log.Errorf("%v", err)
@ -237,30 +328,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
for _, title := range titles { for _, title := range titles {
_title := sqlc.GetTitleByIDRow{ t, err := s.mapTitle(ctx, title)
ID: title.ID,
// StudioID: title.StudioID,
PosterID: title.PosterID,
TitleStatus: title.TitleStatus,
Rating: title.Rating,
RatingCount: title.RatingCount,
ReleaseYear: title.ReleaseYear,
ReleaseSeason: title.ReleaseSeason,
Season: title.Season,
EpisodesAired: title.EpisodesAired,
EpisodesAll: title.EpisodesAll,
// EpisodesLen: title.EpisodesLen,
TitleStorageType: title.TitleStorageType,
TitleImagePath: title.TitleImagePath,
TagNames: title.TitleNames,
StudioName: title.StudioName,
// StudioIllustID: title.StudioIllustID,
// StudioDesc: title.StudioDesc,
// StudioStorageType: title.StudioStorageType,
// StudioImagePath: title.StudioImagePath,
}
t, err := s.mapTitle(ctx, _title)
if err != nil { if err != nil {
log.Errorf("%v", err) log.Errorf("%v", err)
return oapi.GetTitles500Response{}, nil return oapi.GetTitles500Response{}, nil
@ -271,7 +339,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
if request.Params.Sort != nil { if request.Params.Sort != nil {
switch string(*request.Params.Sort) { switch string(*request.Params.Sort) {
case "year": case "year":
tmp := fmt.Sprint(*t.ReleaseYear) tmp := fmt.Sprint("%d", *t.ReleaseYear)
new_cursor.Param = &tmp new_cursor.Param = &tmp
case "rating": case "rating":
tmp := strconv.FormatFloat(*t.Rating, 'f', -1, 64) tmp := strconv.FormatFloat(*t.Rating, 'f', -1, 64)

View file

@ -2,16 +2,12 @@ package handlers
import ( import (
"context" "context"
"fmt"
oapi "nyanimedb/api" oapi "nyanimedb/api"
sqlc "nyanimedb/sql" sqlc "nyanimedb/sql"
"strconv"
"time" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/oapi-codegen/runtime/types" "github.com/oapi-codegen/runtime/types"
log "github.com/sirupsen/logrus"
) )
// type Server struct { // type Server struct {
@ -54,199 +50,34 @@ func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdReque
return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil
} }
func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time {
if p_date.Valid {
t := p_date.Time
return &t
}
return nil
}
type SqlcUserStatus struct {
dropped string
finished string
planned string
in_progress string
}
// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) {
// var sqlc_status SqlcUserStatus
// if s == nil {
// return &sqlc_status, nil
// }
// for _, t := range *s {
// switch t {
// case oapi.UserTitleStatusFinished:
// sqlc_status.finished = "finished"
// case oapi.UserTitleStatusDropped:
// sqlc_status.dropped = "dropped"
// case oapi.UserTitleStatusPlanned:
// sqlc_status.planned = "planned"
// case oapi.UserTitleStatusInProgress:
// sqlc_status.in_progress = "in-progress"
// default:
// return nil, fmt.Errorf("unexpected tittle status: %s", t)
// }
// }
// return &sqlc_status, nil
// }
func sql2usertitlestatus(s sqlc.UsertitleStatusT) (oapi.UserTitleStatus, error) {
var status oapi.UserTitleStatus
switch status {
case "finished":
status = oapi.UserTitleStatusFinished
case "dropped":
status = oapi.UserTitleStatusDropped
case "planned":
status = oapi.UserTitleStatusPlanned
case "in-progress":
status = oapi.UserTitleStatusInProgress
default:
return status, fmt.Errorf("unexpected tittle status: %s", s)
}
return status, nil
}
func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) {
oapi_usertitle := oapi.UserTitle{
Ctime: sqlDate2oapi(t.UserCtime),
Rate: t.UserRate,
ReviewId: t.ReviewID,
// Status: ,
// Title: ,
UserId: t.UserID,
}
status, err := sql2usertitlestatus(t.UsertitleStatus)
if err != nil {
return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err)
}
oapi_usertitle.Status = status
_title := sqlc.GetTitleByIDRow{
ID: t.ID,
// StudioID: title.StudioID,
PosterID: t.PosterID,
TitleStatus: t.TitleStatus,
Rating: t.Rating,
RatingCount: t.RatingCount,
ReleaseYear: t.ReleaseYear,
ReleaseSeason: t.ReleaseSeason,
Season: t.Season,
EpisodesAired: t.EpisodesAired,
EpisodesAll: t.EpisodesAll,
// EpisodesLen: title.EpisodesLen,
TitleStorageType: t.TitleStorageType,
TitleImagePath: t.TitleImagePath,
TagNames: t.TitleNames,
StudioName: t.StudioName,
// StudioIllustID: title.StudioIllustID,
// StudioDesc: title.StudioDesc,
// StudioStorageType: title.StudioStorageType,
// StudioImagePath: title.StudioImagePath,
}
oapi_title, err := s.mapTitle(ctx, _title)
if err != nil {
return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err)
}
oapi_usertitle.Title = &oapi_title
return oapi_usertitle, nil
}
func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) { func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) {
oapi_usertitles := make([]oapi.UserTitle, 0) var rate int32 = 9
var review_id int64 = 3
time := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
word := Word2Sqlc(request.Params.Word) var userTitles = []oapi.UserTitle{
{
season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) UserId: 101,
if err != nil { TitleId: 2001,
log.Errorf("%v", err) Status: oapi.UserTitleStatusFinished,
return oapi.GetUsersUserIdTitles400Response{}, err Rate: &rate,
Ctime: &time,
},
{
UserId: 102,
TitleId: 2002,
Status: oapi.UserTitleStatusInProgress,
ReviewId: &review_id,
Ctime: &time,
},
{
UserId: 103,
TitleId: 2003,
Status: oapi.UserTitleStatusDropped,
Ctime: &time,
},
} }
var statuses_sort []string return oapi.GetUsersUserIdTitles200JSONResponse(userTitles), nil
if request.Params.Status != nil {
for _, s := range *request.Params.Status {
ss := string(s) // s type is alias for string
statuses_sort = append(statuses_sort, ss)
}
}
var watch_status []string
if request.Params.WatchStatus != nil {
for _, s := range *request.Params.WatchStatus {
ss := string(s) // s type is alias for string
watch_status = append(statuses_sort, ss)
}
}
params := sqlc.SearchUserTitlesParams{
Word: word,
TitleStatuses: statuses_sort,
UsertitleStatuses: watch_status,
Rating: request.Params.Rating,
Rate: request.Params.MyRate,
ReleaseYear: request.Params.ReleaseYear,
ReleaseSeason: season,
Forward: true, // default
SortBy: "id", // default
Limit: request.Params.Limit,
}
if request.Params.SortForward != nil {
params.Forward = *request.Params.SortForward
}
if request.Params.Sort != nil {
params.SortBy = string(*request.Params.Sort)
if request.Params.Cursor != nil {
// here we set CursorYear CursorID CursorRating fields
err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), &params)
if err != nil {
log.Errorf("%v", err)
return oapi.GetUsersUserIdTitles400Response{}, nil
}
}
}
// param = nil means it will not be used
titles, err := s.db.SearchUserTitles(ctx, params)
if err != nil {
log.Errorf("%v", err)
return oapi.GetUsersUserIdTitles500Response{}, nil
}
if len(titles) == 0 {
return oapi.GetUsersUserIdTitles204Response{}, nil
}
var new_cursor oapi.CursorObj
for _, title := range titles {
t, err := s.mapUsertitle(ctx, title)
if err != nil {
log.Errorf("%v", err)
return oapi.GetUsersUserIdTitles500Response{}, nil
}
oapi_usertitles = append(oapi_usertitles, t)
new_cursor.Id = t.Title.Id
if request.Params.Sort != nil {
switch string(*request.Params.Sort) {
case "year":
tmp := fmt.Sprint(*t.Title.ReleaseYear)
new_cursor.Param = &tmp
case "rating":
tmp := strconv.FormatFloat(*t.Title.Rating, 'f', -1, 64)
new_cursor.Param = &tmp
}
}
}
return oapi.GetUsersUserIdTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil
} }

View file

@ -95,36 +95,31 @@ LEFT JOIN tags as g ON (tt.tag_id = g.id)
LEFT JOIN studios as s ON (t.studio_id = s.id) LEFT JOIN studios as s ON (t.studio_id = s.id)
LEFT JOIN images as si ON (s.illust_id = si.id) LEFT JOIN images as si ON (s.illust_id = si.id)
WHERE t.id = sqlc.arg('title_id')::bigint WHERE id = sqlc.arg('title_id')::bigint
GROUP BY GROUP BY
t.id, i.id, s.id, si.id; t.id, i.id, s.id, si.id;
-- name: SearchTitles :many -- name: SearchTitles :many
SELECT SELECT
t.id as id, t.*,
t.title_names as title_names,
t.poster_id as poster_id,
t.title_status as title_status,
t.rating as rating,
t.rating_count as rating_count,
t.release_year as release_year,
t.release_season as release_season,
t.season as season,
t.episodes_aired as episodes_aired,
t.episodes_all as episodes_all,
i.storage_type::text as title_storage_type, i.storage_type::text as title_storage_type,
i.image_path as title_image_path, i.image_path as title_image_path,
COALESCE( COALESCE(
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
'[]'::jsonb '[]'::jsonb
)::jsonb as tag_names, )::jsonb as tag_names,
s.studio_name as studio_name s.studio_name as studio_name,
s.illust_id as studio_illust_id,
s.studio_desc as studio_desc,
si.storage_type::text as studio_storage_type,
si.image_path as studio_image_path
FROM titles as t FROM titles as t
LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN images as i ON (t.poster_id = i.id)
LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN tags as g ON (tt.tag_id = g.id)
LEFT JOIN studios as s ON (t.studio_id = s.id) LEFT JOIN studios as s ON (t.studio_id = s.id)
LEFT JOIN images as si ON (s.illust_id = si.id)
WHERE WHERE
CASE CASE
@ -192,19 +187,13 @@ WHERE
END END
) )
AND ( AND (t.title_status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('finished')::text, sqlc.arg('planned')::text))
-- Если массив пуст (NULL или []) — не фильтруем
cardinality(sqlc.arg('title_statuses')::text[]) = 0
OR
-- Иначе: статус есть в списке
t.title_status = ANY(sqlc.arg('title_statuses')::text[])
)
AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float)
AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int)
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
GROUP BY GROUP BY
t.id, i.id, s.id t.id, i.id, s.id, si.id
ORDER BY ORDER BY
CASE WHEN sqlc.arg('forward')::boolean THEN CASE WHEN sqlc.arg('forward')::boolean THEN
@ -227,142 +216,37 @@ ORDER BY
LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit
-- name: SearchUserTitles :many -- name: SearchUserTitles :many
SELECT SELECT
t.id as id, *
t.title_names as title_names, FROM usertitles as u
t.poster_id as poster_id, JOIN titles as t ON (u.title_id = t.id)
t.title_status as title_status, WHERE
t.rating as rating, CASE
t.rating_count as rating_count, WHEN sqlc.narg('word')::text IS NOT NULL THEN
t.release_year as release_year, (
t.release_season as release_season, SELECT bool_and(
t.season as season, EXISTS (
t.episodes_aired as episodes_aired, SELECT 1
t.episodes_all as episodes_all, FROM jsonb_each_text(t.title_names) AS t(key, val)
u.user_id as user_id, WHERE val ILIKE pattern
u.status as usertitle_status, )
u.rate as user_rate, )
u.review_id as review_id, FROM unnest(
u.ctime as user_ctime, ARRAY(
i.storage_type::text as title_storage_type, SELECT '%' || trim(w) || '%'
i.image_path as title_image_path, FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w
COALESCE( WHERE trim(w) <> ''
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), )
'[]'::jsonb ) AS pattern
)::jsonb as tag_names, )
s.studio_name as studio_name ELSE true
FROM usertitles as u
JOIN titles as t ON (u.title_id = t.id)
LEFT JOIN images as i ON (t.poster_id = i.id)
LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
LEFT JOIN tags as g ON (tt.tag_id = g.id)
LEFT JOIN studios as s ON (t.studio_id = s.id)
WHERE
CASE
WHEN sqlc.arg('forward')::boolean THEN
-- forward: greater than cursor (next page)
CASE sqlc.arg('sort_by')::text
WHEN 'year' THEN
(sqlc.narg('cursor_year')::int IS NULL) OR
(t.release_year > sqlc.narg('cursor_year')::int) OR
(t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint)
WHEN 'rating' THEN
(sqlc.narg('cursor_rating')::float IS NULL) OR
(t.rating > sqlc.narg('cursor_rating')::float) OR
(t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint)
WHEN 'id' THEN
(sqlc.narg('cursor_id')::bigint IS NULL) OR
(t.id > sqlc.narg('cursor_id')::bigint)
ELSE true -- fallback
END
ELSE
-- backward: less than cursor (prev page)
CASE sqlc.arg('sort_by')::text
WHEN 'year' THEN
(sqlc.narg('cursor_year')::int IS NULL) OR
(t.release_year < sqlc.narg('cursor_year')::int) OR
(t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint)
WHEN 'rating' THEN
(sqlc.narg('cursor_rating')::float IS NULL) OR
(t.rating < sqlc.narg('cursor_rating')::float) OR
(t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint)
WHEN 'id' THEN
(sqlc.narg('cursor_id')::bigint IS NULL) OR
(t.id < sqlc.narg('cursor_id')::bigint)
ELSE true
END
END END
AND ( AND (sqlc.narg('status')::title_status_t IS NULL OR t.title_status = sqlc.narg('status')::title_status_t)
CASE
WHEN sqlc.narg('word')::text IS NOT NULL THEN
(
SELECT bool_and(
EXISTS (
SELECT 1
FROM jsonb_each_text(t.title_names) AS t(key, val)
WHERE val ILIKE pattern
)
)
FROM unnest(
ARRAY(
SELECT '%' || trim(w) || '%'
FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w
WHERE trim(w) <> ''
)
) AS pattern
)
ELSE true
END
)
AND (
-- Если массив пуст (NULL или []) — не фильтруем
cardinality(sqlc.arg('title_statuses')::text[]) = 0
OR
t.title_status = ANY(sqlc.arg('title_statuses')::text[])
)
AND (
cardinality(sqlc.arg('usertitle_statuses')::text[]) = 0
OR
u.status = ANY(sqlc.arg('usertitle_statuses')::text[])
)
AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int)
AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float)
AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int)
AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t)
AND (sqlc.narg('usertitle_status')::usertitle_status_t IS NULL OR u.usertitle_status = sqlc.narg('usertitle_status')::usertitle_status_t)
GROUP BY
t.id, i.id, s.id
ORDER BY
CASE WHEN sqlc.arg('forward')::boolean THEN
CASE
WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id
WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year
WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating
WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate
END
END ASC,
CASE WHEN NOT sqlc.arg('forward')::boolean THEN
CASE
WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id
WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year
WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating
WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate
END
END DESC,
CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC
LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit

View file

@ -138,7 +138,7 @@ LEFT JOIN tags as g ON (tt.tag_id = g.id)
LEFT JOIN studios as s ON (t.studio_id = s.id) LEFT JOIN studios as s ON (t.studio_id = s.id)
LEFT JOIN images as si ON (s.illust_id = si.id) LEFT JOIN images as si ON (s.illust_id = si.id)
WHERE t.id = $1::bigint WHERE id = $1::bigint
GROUP BY GROUP BY
t.id, i.id, s.id, si.id t.id, i.id, s.id, si.id
` `
@ -342,30 +342,25 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams
const searchTitles = `-- name: SearchTitles :many const searchTitles = `-- name: SearchTitles :many
SELECT SELECT
t.id as id, t.id, t.title_names, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len,
t.title_names as title_names,
t.poster_id as poster_id,
t.title_status as title_status,
t.rating as rating,
t.rating_count as rating_count,
t.release_year as release_year,
t.release_season as release_season,
t.season as season,
t.episodes_aired as episodes_aired,
t.episodes_all as episodes_all,
i.storage_type::text as title_storage_type, i.storage_type::text as title_storage_type,
i.image_path as title_image_path, i.image_path as title_image_path,
COALESCE( COALESCE(
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL),
'[]'::jsonb '[]'::jsonb
)::jsonb as tag_names, )::jsonb as tag_names,
s.studio_name as studio_name s.studio_name as studio_name,
s.illust_id as studio_illust_id,
s.studio_desc as studio_desc,
si.storage_type::text as studio_storage_type,
si.image_path as studio_image_path
FROM titles as t FROM titles as t
LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN images as i ON (t.poster_id = i.id)
LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN tags as g ON (tt.tag_id = g.id)
LEFT JOIN studios as s ON (t.studio_id = s.id) LEFT JOIN studios as s ON (t.studio_id = s.id)
LEFT JOIN images as si ON (s.illust_id = si.id)
WHERE WHERE
CASE CASE
@ -433,19 +428,13 @@ WHERE
END END
) )
AND ( AND (t.title_status::text IN ($7::text, $8::text, $9::text))
-- Если массив пуст (NULL или []) не фильтруем AND ($10::float IS NULL OR t.rating >= $10::float)
cardinality($7::text[]) = 0 AND ($11::int IS NULL OR t.release_year = $11::int)
OR AND ($12::release_season_t IS NULL OR t.release_season = $12::release_season_t)
-- Иначе: статус есть в списке
t.title_status = ANY($7::text[])
)
AND ($8::float IS NULL OR t.rating >= $8::float)
AND ($9::int IS NULL OR t.release_year = $9::int)
AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t)
GROUP BY GROUP BY
t.id, i.id, s.id t.id, i.id, s.id, si.id
ORDER BY ORDER BY
CASE WHEN $1::boolean THEN CASE WHEN $1::boolean THEN
@ -465,7 +454,7 @@ ORDER BY
CASE WHEN $2::text <> 'id' THEN t.id END ASC CASE WHEN $2::text <> 'id' THEN t.id END ASC
LIMIT COALESCE($11::int, 100) LIMIT COALESCE($13::int, 100)
` `
type SearchTitlesParams struct { type SearchTitlesParams struct {
@ -475,7 +464,9 @@ type SearchTitlesParams struct {
CursorID *int64 `json:"cursor_id"` CursorID *int64 `json:"cursor_id"`
CursorRating *float64 `json:"cursor_rating"` CursorRating *float64 `json:"cursor_rating"`
Word *string `json:"word"` Word *string `json:"word"`
TitleStatuses []string `json:"title_statuses"` Ongoing string `json:"ongoing"`
Finished string `json:"finished"`
Planned string `json:"planned"`
Rating *float64 `json:"rating"` Rating *float64 `json:"rating"`
ReleaseYear *int32 `json:"release_year"` ReleaseYear *int32 `json:"release_year"`
ReleaseSeason *ReleaseSeasonT `json:"release_season"` ReleaseSeason *ReleaseSeasonT `json:"release_season"`
@ -483,21 +474,27 @@ type SearchTitlesParams struct {
} }
type SearchTitlesRow struct { type SearchTitlesRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
TitleNames json.RawMessage `json:"title_names"` TitleNames json.RawMessage `json:"title_names"`
PosterID *int64 `json:"poster_id"` StudioID int64 `json:"studio_id"`
TitleStatus TitleStatusT `json:"title_status"` PosterID *int64 `json:"poster_id"`
Rating *float64 `json:"rating"` TitleStatus TitleStatusT `json:"title_status"`
RatingCount *int32 `json:"rating_count"` Rating *float64 `json:"rating"`
ReleaseYear *int32 `json:"release_year"` RatingCount *int32 `json:"rating_count"`
ReleaseSeason *ReleaseSeasonT `json:"release_season"` ReleaseYear *int32 `json:"release_year"`
Season *int32 `json:"season"` ReleaseSeason *ReleaseSeasonT `json:"release_season"`
EpisodesAired *int32 `json:"episodes_aired"` Season *int32 `json:"season"`
EpisodesAll *int32 `json:"episodes_all"` EpisodesAired *int32 `json:"episodes_aired"`
TitleStorageType string `json:"title_storage_type"` EpisodesAll *int32 `json:"episodes_all"`
TitleImagePath *string `json:"title_image_path"` EpisodesLen []byte `json:"episodes_len"`
TagNames json.RawMessage `json:"tag_names"` TitleStorageType string `json:"title_storage_type"`
StudioName *string `json:"studio_name"` TitleImagePath *string `json:"title_image_path"`
TagNames json.RawMessage `json:"tag_names"`
StudioName *string `json:"studio_name"`
StudioIllustID *int64 `json:"studio_illust_id"`
StudioDesc *string `json:"studio_desc"`
StudioStorageType string `json:"studio_storage_type"`
StudioImagePath *string `json:"studio_image_path"`
} }
func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]SearchTitlesRow, error) { func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]SearchTitlesRow, error) {
@ -508,7 +505,9 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
arg.CursorID, arg.CursorID,
arg.CursorRating, arg.CursorRating,
arg.Word, arg.Word,
arg.TitleStatuses, arg.Ongoing,
arg.Finished,
arg.Planned,
arg.Rating, arg.Rating,
arg.ReleaseYear, arg.ReleaseYear,
arg.ReleaseSeason, arg.ReleaseSeason,
@ -524,6 +523,7 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.TitleNames, &i.TitleNames,
&i.StudioID,
&i.PosterID, &i.PosterID,
&i.TitleStatus, &i.TitleStatus,
&i.Rating, &i.Rating,
@ -533,10 +533,15 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
&i.Season, &i.Season,
&i.EpisodesAired, &i.EpisodesAired,
&i.EpisodesAll, &i.EpisodesAll,
&i.EpisodesLen,
&i.TitleStorageType, &i.TitleStorageType,
&i.TitleImagePath, &i.TitleImagePath,
&i.TagNames, &i.TagNames,
&i.StudioName, &i.StudioName,
&i.StudioIllustID,
&i.StudioDesc,
&i.StudioStorageType,
&i.StudioImagePath,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -550,200 +555,82 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
const searchUserTitles = `-- name: SearchUserTitles :many const searchUserTitles = `-- name: SearchUserTitles :many
SELECT SELECT
t.id as id, user_id, title_id, status, rate, review_id, ctime, id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len
t.title_names as title_names, FROM usertitles as u
t.poster_id as poster_id, JOIN titles as t ON (u.title_id = t.id)
t.title_status as title_status, WHERE
t.rating as rating, CASE
t.rating_count as rating_count, WHEN $1::text IS NOT NULL THEN
t.release_year as release_year, (
t.release_season as release_season, SELECT bool_and(
t.season as season, EXISTS (
t.episodes_aired as episodes_aired, SELECT 1
t.episodes_all as episodes_all, FROM jsonb_each_text(t.title_names) AS t(key, val)
u.user_id as user_id, WHERE val ILIKE pattern
u.status as usertitle_status, )
u.rate as user_rate, )
u.review_id as review_id, FROM unnest(
u.ctime as user_ctime, ARRAY(
i.storage_type::text as title_storage_type, SELECT '%' || trim(w) || '%'
i.image_path as title_image_path, FROM unnest(string_to_array($1::text, ' ')) AS w
COALESCE( WHERE trim(w) <> ''
jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), )
'[]'::jsonb ) AS pattern
)::jsonb as tag_names, )
s.studio_name as studio_name ELSE true
FROM usertitles as u
JOIN titles as t ON (u.title_id = t.id)
LEFT JOIN images as i ON (t.poster_id = i.id)
LEFT JOIN title_tags as tt ON (t.id = tt.title_id)
LEFT JOIN tags as g ON (tt.tag_id = g.id)
LEFT JOIN studios as s ON (t.studio_id = s.id)
WHERE
CASE
WHEN $1::boolean THEN
-- forward: greater than cursor (next page)
CASE $2::text
WHEN 'year' THEN
($3::int IS NULL) OR
(t.release_year > $3::int) OR
(t.release_year = $3::int AND t.id > $4::bigint)
WHEN 'rating' THEN
($5::float IS NULL) OR
(t.rating > $5::float) OR
(t.rating = $5::float AND t.id > $4::bigint)
WHEN 'id' THEN
($4::bigint IS NULL) OR
(t.id > $4::bigint)
ELSE true -- fallback
END
ELSE
-- backward: less than cursor (prev page)
CASE $2::text
WHEN 'year' THEN
($3::int IS NULL) OR
(t.release_year < $3::int) OR
(t.release_year = $3::int AND t.id < $4::bigint)
WHEN 'rating' THEN
($5::float IS NULL) OR
(t.rating < $5::float) OR
(t.rating = $5::float AND t.id < $4::bigint)
WHEN 'id' THEN
($4::bigint IS NULL) OR
(t.id < $4::bigint)
ELSE true
END
END END
AND ( AND ($2::title_status_t IS NULL OR t.title_status = $2::title_status_t)
CASE AND ($3::float IS NULL OR t.rating >= $3::float)
WHEN $6::text IS NOT NULL THEN AND ($4::int IS NULL OR t.release_year = $4::int)
( AND ($5::release_season_t IS NULL OR t.release_season = $5::release_season_t)
SELECT bool_and( AND ($6::usertitle_status_t IS NULL OR u.usertitle_status = $6::usertitle_status_t)
EXISTS (
SELECT 1
FROM jsonb_each_text(t.title_names) AS t(key, val)
WHERE val ILIKE pattern
)
)
FROM unnest(
ARRAY(
SELECT '%' || trim(w) || '%'
FROM unnest(string_to_array($6::text, ' ')) AS w
WHERE trim(w) <> ''
)
) AS pattern
)
ELSE true
END
)
AND ( LIMIT COALESCE($7::int, 100)
-- Если массив пуст (NULL или []) не фильтруем
cardinality($7::text[]) = 0
OR
t.title_status = ANY($7::text[])
)
AND (
cardinality($8::text[]) = 0
OR
u.status = ANY($8::text[])
)
AND ($9::int IS NULL OR u.rate >= $9::int)
AND ($10::float IS NULL OR t.rating >= $10::float)
AND ($11::int IS NULL OR t.release_year = $11::int)
AND ($12::release_season_t IS NULL OR t.release_season = $12::release_season_t)
GROUP BY
t.id, i.id, s.id
ORDER BY
CASE WHEN $1::boolean THEN
CASE
WHEN $2::text = 'id' THEN t.id
WHEN $2::text = 'year' THEN t.release_year
WHEN $2::text = 'rating' THEN t.rating
WHEN $2::text = 'rate' THEN u.rate
END
END ASC,
CASE WHEN NOT $1::boolean THEN
CASE
WHEN $2::text = 'id' THEN t.id
WHEN $2::text = 'year' THEN t.release_year
WHEN $2::text = 'rating' THEN t.rating
WHEN $2::text = 'rate' THEN u.rate
END
END DESC,
CASE WHEN $2::text <> 'id' THEN t.id END ASC
LIMIT COALESCE($13::int, 100)
` `
type SearchUserTitlesParams struct { type SearchUserTitlesParams struct {
Forward bool `json:"forward"` Word *string `json:"word"`
SortBy string `json:"sort_by"` Status *TitleStatusT `json:"status"`
CursorYear *int32 `json:"cursor_year"` Rating *float64 `json:"rating"`
CursorID *int64 `json:"cursor_id"` ReleaseYear *int32 `json:"release_year"`
CursorRating *float64 `json:"cursor_rating"` ReleaseSeason *ReleaseSeasonT `json:"release_season"`
Word *string `json:"word"` UsertitleStatus NullUsertitleStatusT `json:"usertitle_status"`
TitleStatuses []string `json:"title_statuses"` Limit *int32 `json:"limit"`
UsertitleStatuses []string `json:"usertitle_statuses"`
Rate *int32 `json:"rate"`
Rating *float64 `json:"rating"`
ReleaseYear *int32 `json:"release_year"`
ReleaseSeason *ReleaseSeasonT `json:"release_season"`
Limit *int32 `json:"limit"`
} }
type SearchUserTitlesRow struct { type SearchUserTitlesRow struct {
ID int64 `json:"id"` UserID int64 `json:"user_id"`
TitleNames json.RawMessage `json:"title_names"` TitleID int64 `json:"title_id"`
PosterID *int64 `json:"poster_id"` Status UsertitleStatusT `json:"status"`
TitleStatus TitleStatusT `json:"title_status"` Rate *int32 `json:"rate"`
Rating *float64 `json:"rating"` ReviewID *int64 `json:"review_id"`
RatingCount *int32 `json:"rating_count"` Ctime pgtype.Timestamptz `json:"ctime"`
ReleaseYear *int32 `json:"release_year"` ID int64 `json:"id"`
ReleaseSeason *ReleaseSeasonT `json:"release_season"` TitleNames json.RawMessage `json:"title_names"`
Season *int32 `json:"season"` StudioID int64 `json:"studio_id"`
EpisodesAired *int32 `json:"episodes_aired"` PosterID *int64 `json:"poster_id"`
EpisodesAll *int32 `json:"episodes_all"` TitleStatus TitleStatusT `json:"title_status"`
UserID int64 `json:"user_id"` Rating *float64 `json:"rating"`
UsertitleStatus UsertitleStatusT `json:"usertitle_status"` RatingCount *int32 `json:"rating_count"`
UserRate *int32 `json:"user_rate"` ReleaseYear *int32 `json:"release_year"`
ReviewID *int64 `json:"review_id"` ReleaseSeason *ReleaseSeasonT `json:"release_season"`
UserCtime pgtype.Timestamptz `json:"user_ctime"` Season *int32 `json:"season"`
TitleStorageType string `json:"title_storage_type"` EpisodesAired *int32 `json:"episodes_aired"`
TitleImagePath *string `json:"title_image_path"` EpisodesAll *int32 `json:"episodes_all"`
TagNames json.RawMessage `json:"tag_names"` EpisodesLen []byte `json:"episodes_len"`
StudioName *string `json:"studio_name"`
} }
// 100 is default limit // 100 is default limit
func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) {
rows, err := q.db.Query(ctx, searchUserTitles, rows, err := q.db.Query(ctx, searchUserTitles,
arg.Forward,
arg.SortBy,
arg.CursorYear,
arg.CursorID,
arg.CursorRating,
arg.Word, arg.Word,
arg.TitleStatuses, arg.Status,
arg.UsertitleStatuses,
arg.Rate,
arg.Rating, arg.Rating,
arg.ReleaseYear, arg.ReleaseYear,
arg.ReleaseSeason, arg.ReleaseSeason,
arg.UsertitleStatus,
arg.Limit, arg.Limit,
) )
if err != nil { if err != nil {
@ -754,8 +641,15 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara
for rows.Next() { for rows.Next() {
var i SearchUserTitlesRow var i SearchUserTitlesRow
if err := rows.Scan( if err := rows.Scan(
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
&i.Ctime,
&i.ID, &i.ID,
&i.TitleNames, &i.TitleNames,
&i.StudioID,
&i.PosterID, &i.PosterID,
&i.TitleStatus, &i.TitleStatus,
&i.Rating, &i.Rating,
@ -765,15 +659,7 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara
&i.Season, &i.Season,
&i.EpisodesAired, &i.EpisodesAired,
&i.EpisodesAll, &i.EpisodesAll,
&i.UserID, &i.EpisodesLen,
&i.UsertitleStatus,
&i.UserRate,
&i.ReviewID,
&i.UserCtime,
&i.TitleStorageType,
&i.TitleImagePath,
&i.TagNames,
&i.StudioName,
); err != nil { ); err != nil {
return nil, err return nil, err
} }