Add Danime episode metadata fetching and related API endpoint
- Implemented FetchEpisode function to retrieve episode metadata from dアニメストア. - Added /api/v1/danime GET endpoint to fetch episode details by partId. - Updated Swagger documentation for the new endpoint and response structure. - Created DanimeEpisodeResponse type for API responses. - Added tests for the new functionality and handlers.
This commit is contained in:
parent
10c54e9821
commit
88f4ee3d4a
37
backend/cmd/danime-episode/main.go
Normal file
37
backend/cmd/danime-episode/main.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"watch-party-backend/internal/danime"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s <partId>\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
partID := flag.Arg(0)
|
||||
|
||||
ep, err := danime.FetchEpisode(context.Background(), partID)
|
||||
if err != nil {
|
||||
log.Fatalf("fetch failed: %v", err)
|
||||
}
|
||||
|
||||
buf, err := json.MarshalIndent(ep, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshal failed: %v", err)
|
||||
}
|
||||
fmt.Println(string(buf))
|
||||
}
|
||||
@ -143,6 +143,47 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/danime": {
|
||||
"get": {
|
||||
"description": "Fetch metadata from dアニメストア WS030101 by partId.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"scraper"
|
||||
],
|
||||
"summary": "Fetch dアニメ episode metadata",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "dアニメ partId",
|
||||
"name": "part_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/httpapi.DanimeEpisodeResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "missing part_id",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/httpapi.HTTPError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "scrape failed",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/httpapi.HTTPError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/shows": {
|
||||
"get": {
|
||||
"description": "List all rows from ` + "`" + `current` + "`" + ` table.",
|
||||
@ -330,6 +371,27 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"httpapi.DanimeEpisodeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ep_num": {
|
||||
"type": "integer",
|
||||
"example": 21
|
||||
},
|
||||
"ep_title": {
|
||||
"type": "string",
|
||||
"example": "あったんだ。確かに"
|
||||
},
|
||||
"playback_length": {
|
||||
"type": "string",
|
||||
"example": "00:23:50"
|
||||
},
|
||||
"season_name": {
|
||||
"type": "string",
|
||||
"example": "フルーツバスケット 2nd season"
|
||||
}
|
||||
}
|
||||
},
|
||||
"httpapi.HTTPError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -141,6 +141,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/danime": {
|
||||
"get": {
|
||||
"description": "Fetch metadata from dアニメストア WS030101 by partId.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"scraper"
|
||||
],
|
||||
"summary": "Fetch dアニメ episode metadata",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "dアニメ partId",
|
||||
"name": "part_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/httpapi.DanimeEpisodeResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "missing part_id",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/httpapi.HTTPError"
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "scrape failed",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/httpapi.HTTPError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/shows": {
|
||||
"get": {
|
||||
"description": "List all rows from `current` table.",
|
||||
@ -328,6 +369,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"httpapi.DanimeEpisodeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ep_num": {
|
||||
"type": "integer",
|
||||
"example": 21
|
||||
},
|
||||
"ep_title": {
|
||||
"type": "string",
|
||||
"example": "あったんだ。確かに"
|
||||
},
|
||||
"playback_length": {
|
||||
"type": "string",
|
||||
"example": "00:23:50"
|
||||
},
|
||||
"season_name": {
|
||||
"type": "string",
|
||||
"example": "フルーツバスケット 2nd season"
|
||||
}
|
||||
}
|
||||
},
|
||||
"httpapi.HTTPError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -51,6 +51,21 @@ definitions:
|
||||
example: "10:00:00"
|
||||
type: string
|
||||
type: object
|
||||
httpapi.DanimeEpisodeResponse:
|
||||
properties:
|
||||
ep_num:
|
||||
example: 21
|
||||
type: integer
|
||||
ep_title:
|
||||
example: あったんだ。確かに
|
||||
type: string
|
||||
playback_length:
|
||||
example: "00:23:50"
|
||||
type: string
|
||||
season_name:
|
||||
example: フルーツバスケット 2nd season
|
||||
type: string
|
||||
type: object
|
||||
httpapi.HTTPError:
|
||||
properties:
|
||||
error:
|
||||
@ -191,6 +206,33 @@ paths:
|
||||
summary: Set current show
|
||||
tags:
|
||||
- current
|
||||
/api/v1/danime:
|
||||
get:
|
||||
description: Fetch metadata from dアニメストア WS030101 by partId.
|
||||
parameters:
|
||||
- description: dアニメ partId
|
||||
in: query
|
||||
name: part_id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/httpapi.DanimeEpisodeResponse'
|
||||
"400":
|
||||
description: missing part_id
|
||||
schema:
|
||||
$ref: '#/definitions/httpapi.HTTPError'
|
||||
"502":
|
||||
description: scrape failed
|
||||
schema:
|
||||
$ref: '#/definitions/httpapi.HTTPError'
|
||||
summary: Fetch dアニメ episode metadata
|
||||
tags:
|
||||
- scraper
|
||||
/api/v1/shows:
|
||||
delete:
|
||||
description: Delete a row from `current` by ID.
|
||||
|
||||
151
backend/internal/danime/danime.go
Normal file
151
backend/internal/danime/danime.go
Normal file
@ -0,0 +1,151 @@
|
||||
package danime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RawPartInfo mirrors the subset of fields we need from WS030101.
|
||||
type RawPartInfo struct {
|
||||
WorkTitle string `json:"workTitle"` // Season / series title
|
||||
PartTitle string `json:"partTitle"` // Episode title
|
||||
PartDispNumber string `json:"partDispNumber"` // Display episode number, e.g. "第21話" or "#21"
|
||||
PartMeasure string `json:"partMeasure"` // Duration string, e.g. "23分50秒"
|
||||
}
|
||||
|
||||
// Episode is the normalized representation we use internally and/or output as JSON.
|
||||
type Episode struct {
|
||||
EpNum int `json:"ep_num"`
|
||||
EpTitle string `json:"ep_title"`
|
||||
SeasonName string `json:"season_name"`
|
||||
PlaybackLength string `json:"playback_length"` // format: "HH:MM:SS"
|
||||
}
|
||||
|
||||
var (
|
||||
baseURL = "https://animestore.docomo.ne.jp/animestore/rest/WS030101"
|
||||
httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
epNumRe = regexp.MustCompile(`\d+`)
|
||||
hourRe = regexp.MustCompile(`(\d+)時間`)
|
||||
minRe = regexp.MustCompile(`(\d+)分`)
|
||||
secRe = regexp.MustCompile(`(\d+)秒`)
|
||||
)
|
||||
|
||||
// FetchEpisode fetches metadata for a single episode (partId) and returns normalized Episode.
|
||||
func FetchEpisode(ctx context.Context, partID string) (*Episode, error) {
|
||||
partID = strings.TrimSpace(partID)
|
||||
if partID == "" {
|
||||
return nil, fmt.Errorf("partId is required")
|
||||
}
|
||||
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse base url: %w", err)
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("partId", partID)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching WS030101 for partId=%s: %w", partID, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("WS030101 status %d for partId=%s", resp.StatusCode, partID)
|
||||
}
|
||||
|
||||
var raw RawPartInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||
return nil, fmt.Errorf("decoding WS030101: %w", err)
|
||||
}
|
||||
|
||||
ep, err := normalizeEpisode(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("normalizing WS030101: %w", err)
|
||||
}
|
||||
return &ep, nil
|
||||
}
|
||||
|
||||
func normalizeEpisode(raw RawPartInfo) (Episode, error) {
|
||||
num := parseEpisodeNumber(raw.PartDispNumber)
|
||||
length, err := parsePartMeasure(raw.PartMeasure)
|
||||
if err != nil {
|
||||
return Episode{}, err
|
||||
}
|
||||
|
||||
return Episode{
|
||||
EpNum: num,
|
||||
EpTitle: raw.PartTitle,
|
||||
SeasonName: raw.WorkTitle,
|
||||
PlaybackLength: length,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseEpisodeNumber(s string) int {
|
||||
s = toHalfWidthDigits(s)
|
||||
match := epNumRe.FindString(s)
|
||||
if match == "" {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(match)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func parsePartMeasure(s string) (string, error) {
|
||||
s = strings.TrimSpace(toHalfWidthDigits(s))
|
||||
if s == "" {
|
||||
return "", fmt.Errorf("partMeasure missing")
|
||||
}
|
||||
|
||||
hours := extractInt(hourRe, s)
|
||||
minutes := extractInt(minRe, s)
|
||||
seconds := extractInt(secRe, s)
|
||||
|
||||
if hours == 0 && minutes == 0 && seconds == 0 {
|
||||
return "", fmt.Errorf("could not parse partMeasure %q", s)
|
||||
}
|
||||
|
||||
dur := time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second
|
||||
totalSeconds := int(dur.Seconds())
|
||||
h := totalSeconds / 3600
|
||||
m := (totalSeconds % 3600) / 60
|
||||
sec := totalSeconds % 60
|
||||
return fmt.Sprintf("%02d:%02d:%02d", h, m, sec), nil
|
||||
}
|
||||
|
||||
func extractInt(re *regexp.Regexp, s string) int {
|
||||
m := re.FindStringSubmatch(s)
|
||||
if len(m) < 2 {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func toHalfWidthDigits(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
// Convert full-width digits to ASCII digits.
|
||||
if r >= '0' && r <= '9' {
|
||||
return r - '0' + '0'
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
118
backend/internal/danime/danime_test.go
Normal file
118
backend/internal/danime/danime_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
package danime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEpisodeNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want int
|
||||
}{
|
||||
{"第21話", 21},
|
||||
{"第02話", 2},
|
||||
{"21", 21},
|
||||
{"第21話「タイトル」", 21},
|
||||
{"#21", 21},
|
||||
{"話", 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := parseEpisodeNumber(tt.in); got != tt.want {
|
||||
t.Fatalf("parseEpisodeNumber(%q) = %d, want %d", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePartMeasure(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"23分50秒", "00:23:50"},
|
||||
{"1時間2分3秒", "01:02:03"},
|
||||
{"5分", "00:05:00"},
|
||||
{"45秒", "00:00:45"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := parsePartMeasure(tt.in)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePartMeasure(%q) error: %v", tt.in, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("parsePartMeasure(%q) = %s, want %s", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeEpisode_FromFixture(t *testing.T) {
|
||||
f, err := os.Open("testdata/ws030101_part_23863021.json")
|
||||
if err != nil {
|
||||
t.Fatalf("open fixture: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var raw RawPartInfo
|
||||
if err := json.NewDecoder(f).Decode(&raw); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
|
||||
ep, err := normalizeEpisode(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize: %v", err)
|
||||
}
|
||||
if ep.EpNum != 21 || ep.EpTitle == "" || ep.PlaybackLength != "00:23:50" {
|
||||
t.Fatalf("unexpected episode: %+v", ep)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchEpisode_UsesHTTPClient(t *testing.T) {
|
||||
rt := roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Query().Get("partId") != "999" {
|
||||
t.Fatalf("expected partId=999, got %s", r.URL.RawQuery)
|
||||
}
|
||||
f, err := os.Open("testdata/ws030101_part_23863021.json")
|
||||
if err != nil {
|
||||
t.Fatalf("open fixture: %v", err)
|
||||
}
|
||||
body, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})
|
||||
mockClient := &http.Client{Transport: rt}
|
||||
|
||||
prevBase := baseURL
|
||||
prevClient := httpClient
|
||||
baseURL = "http://example.com/WS030101"
|
||||
httpClient = mockClient
|
||||
defer func() {
|
||||
baseURL = prevBase
|
||||
httpClient = prevClient
|
||||
}()
|
||||
|
||||
ep, err := FetchEpisode(context.Background(), "999")
|
||||
if err != nil {
|
||||
t.Fatalf("FetchEpisode error: %v", err)
|
||||
}
|
||||
if ep == nil || ep.EpNum != 21 {
|
||||
t.Fatalf("unexpected episode: %+v", ep)
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"watch-party-backend/internal/core/episode"
|
||||
"watch-party-backend/internal/danime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -28,6 +29,7 @@ func NewRouter(svc episode.UseCases) *gin.Engine {
|
||||
v1.POST("/shows", createShowHandler(svc))
|
||||
v1.GET("/shows", listShowsHandler(svc))
|
||||
v1.DELETE("/shows", deleteShowsHandler(svc))
|
||||
v1.GET("/danime", getDanimeHandler())
|
||||
|
||||
return r
|
||||
}
|
||||
@ -157,6 +159,39 @@ func createShowHandler(svc episode.UseCases) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
var FetchDanimeEpisode = danime.FetchEpisode
|
||||
|
||||
// getDanimeHandler godoc
|
||||
// @Summary Fetch dアニメ episode metadata
|
||||
// @Description Fetch metadata from dアニメストア WS030101 by partId.
|
||||
// @Tags scraper
|
||||
// @Produce json
|
||||
// @Param part_id query string true "dアニメ partId"
|
||||
// @Success 200 {object} DanimeEpisodeResponse
|
||||
// @Failure 400 {object} HTTPError "missing part_id"
|
||||
// @Failure 502 {object} HTTPError "scrape failed"
|
||||
// @Router /api/v1/danime [get]
|
||||
func getDanimeHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
partID := strings.TrimSpace(c.Query("part_id"))
|
||||
if partID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "part_id is required"})
|
||||
return
|
||||
}
|
||||
ep, err := FetchDanimeEpisode(c.Request.Context(), partID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "scrape failed"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, DanimeEpisodeResponse{
|
||||
EpNum: ep.EpNum,
|
||||
EpTitle: ep.EpTitle,
|
||||
SeasonName: ep.SeasonName,
|
||||
PlaybackLength: ep.PlaybackLength,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// moveToArchiveHandler godoc
|
||||
// @Summary Move shows to archive
|
||||
// @Description Move one or many IDs from `current` to `current_archive`.
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"watch-party-backend/internal/core/episode"
|
||||
"watch-party-backend/internal/danime"
|
||||
httpapi "watch-party-backend/internal/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -321,6 +322,60 @@ func TestPostShows_DefaultStartTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDanime_MissingParam(t *testing.T) {
|
||||
r := newRouterWithSvc(&fakeSvc{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/danime", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDanime_Error(t *testing.T) {
|
||||
prev := httpapi.FetchDanimeEpisode
|
||||
httpapi.FetchDanimeEpisode = func(ctx context.Context, partID string) (*danime.Episode, error) {
|
||||
return nil, errors.New("boom")
|
||||
}
|
||||
defer func() { httpapi.FetchDanimeEpisode = prev }()
|
||||
|
||||
r := newRouterWithSvc(&fakeSvc{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/danime?part_id=123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("expected 502, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDanime_OK(t *testing.T) {
|
||||
prev := httpapi.FetchDanimeEpisode
|
||||
httpapi.FetchDanimeEpisode = func(ctx context.Context, partID string) (*danime.Episode, error) {
|
||||
return &danime.Episode{
|
||||
EpNum: 21,
|
||||
EpTitle: "Title",
|
||||
SeasonName: "Season",
|
||||
PlaybackLength: "00:23:50",
|
||||
}, nil
|
||||
}
|
||||
defer func() { httpapi.FetchDanimeEpisode = prev }()
|
||||
|
||||
r := newRouterWithSvc(&fakeSvc{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/danime?part_id=23863021", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var got danime.Episode
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("json: %v", err)
|
||||
}
|
||||
if got.EpNum != 21 || got.PlaybackLength != "00:23:50" {
|
||||
t.Fatalf("unexpected body: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListShows_OK(t *testing.T) {
|
||||
svc := &fakeSvc{
|
||||
listRes: []episode.Episode{
|
||||
|
||||
@ -49,3 +49,11 @@ type CurrentResponse struct {
|
||||
CurrentEp bool `json:"current_ep" example:"false"`
|
||||
DateCreated time.Time `json:"date_created" example:"2024-02-01T15:04:05Z"`
|
||||
}
|
||||
|
||||
// DanimeEpisodeResponse is the response body for GET /api/v1/danime.
|
||||
type DanimeEpisodeResponse struct {
|
||||
EpNum int `json:"ep_num" example:"21"`
|
||||
EpTitle string `json:"ep_title" example:"あったんだ。確かに"`
|
||||
SeasonName string `json:"season_name" example:"フルーツバスケット 2nd season"`
|
||||
PlaybackLength string `json:"playback_length" example:"00:23:50"`
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user