Skip to content
This repository has been archived by the owner on Sep 22, 2023. It is now read-only.

Add review endpoint #1

Merged
merged 10 commits into from
Jul 14, 2022
5 changes: 5 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package crunchyroll

type BulkResult[T any] struct {
Items []T `json:"items"`
Total int `json:"total"`
}

type Image struct {
Height int `json:"height"`
Source string `json:"source"`
Expand Down
212 changes: 212 additions & 0 deletions review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package crunchyroll

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)

// ReviewRating represents stars for a series rating from one to five.
type ReviewRating string

const (
OneStar ReviewRating = "s1"
TwoStars = "s2"
ThreeStars = "s3"
FourStars = "s4"
FiveStars = "s5"
)

type ratingStar struct {
Displayed string `json:"displayed"`
Unit string `json:"unit"`
Percentage int `json:"percentage"`
}

// Rating represents the overall rating of a series.
type Rating struct {
OneStar ratingStar `json:"1s"`
TwoStars ratingStar `json:"2s"`
ThreeStars ratingStar `json:"3s"`
FourStars ratingStar `json:"4s"`
FiveStars ratingStar `json:"5s"`
Average string `json:"average"`
Total int `json:"total"`
Rating string `json:"rating"`
}

// Review is the interface which gets implemented by OwnerReview and UserReview.
type Review interface{}

type review struct {
crunchy *Crunchyroll

SeriesID string

ReviewData struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Language LOCALE `json:"language"`
CreatedAt time.Time `json:"created_at"`
ModifiedAt time.Time `json:"modified_at"`
AuthoredReviews int `json:"authored_reviews"`
Spoiler bool `json:"spoiler"`
} `json:"review"`
AuthorRating ReviewRating `json:"author_rating"`
Author struct {
Username string `json:"username"`
Avatar string `json:"avatar"`
ID string `json:"ID"`
} `json:"author"`
Ratings struct {
Yes struct {
Displayed string `json:"displayed"`
Unit string `json:"unit"`
} `json:"yes"`
No struct {
Displayed string `json:"displayed"`
Unit string `json:"unit"`
} `json:"no"`
Total string `json:"total"`
// yes or no so basically a bool if set
Rating string `json:"rating"`
Reported bool `json:"reported"`
} `json:"ratings"`
}

// OwnerReview is a series review which has been written from the current logged-in user.
type OwnerReview struct {
Review

*review
}

// Edit edits the review from the logged in account.
func (or *OwnerReview) Edit(title, content string, spoiler bool) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/en-US/user/%s/review/series/%s", or.crunchy.Config.AccountID, or.SeriesID)
body, _ := json.Marshal(map[string]any{
"title": title,
"body": content,
"spoiler": spoiler,
})
req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
resp, err := or.crunchy.requestFull(req)
if err != nil {
return err
}
defer resp.Body.Close()

json.NewDecoder(resp.Body).Decode(or)

return nil
}

// Delete deletes the review from the logged in account.
func (or *OwnerReview) Delete() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/en-US/user/%s/review/series/%s", or.crunchy.Config.AccountID, or.SeriesID)
_, err := or.crunchy.request(endpoint, http.MethodDelete)
return err
}

// UserReview is a series review written from other crunchyroll users.
type UserReview struct {
Review

*review
}

// RateHelpful rates the review as helpful. A review can only be rated once
// as helpful (or not helpful) and this cannot be undone, so be careful. Use
// Rated to see if the review was already rated.
func (ur *UserReview) RateHelpful() error {
return ur.rate(true)
}

// RateNotHelpful rates the review as not helpful. A review can only be rated
// once as helpful (or not helpful) and this cannot be undone, so be careful.
// Use Rated to see if the review was already rated.
func (ur *UserReview) RateNotHelpful() error {
return ur.rate(false)
}

// Rated returns if the user already rated the review (with RateHelpful or
// RateNotHelpful).
func (ur *UserReview) Rated() bool {
return ur.Ratings.Rating != ""
}

func (ur *UserReview) rate(positive bool) error {
if ur.Rated() {
var humanReadable string
switch ur.Ratings.Rating {
case "yes":
humanReadable = "helpful"
case "no":
humanReadable = "not helpful"
}
return fmt.Errorf("review is already rated as %s", humanReadable)
}

endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/user/%s/rating/review/%s", ur.crunchy.Config.AccountID, ur.ReviewData.ID)
var body []byte
if positive {
body, _ = json.Marshal(map[string]string{"rate": "yes"})
} else {
body, _ = json.Marshal(map[string]string{"rate": "no"})
}
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
resp, err := ur.crunchy.requestFull(req)
if err != nil {
return err
}
defer resp.Body.Close()

json.NewDecoder(resp.Body).Decode(&ur.Ratings)

return nil
}

// Report reports the review. Only works if the review hasn't been reported yet.
// See UserReview.Ratings.Reported if it is already reported.
func (ur *UserReview) Report() error {
if ur.Ratings.Reported {
return fmt.Errorf("review is already reported")
}
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/user/%s/report/review/%s", ur.crunchy.Config.AccountID, ur.ReviewData.ID)
_, err := ur.crunchy.request(endpoint, http.MethodPut)
if err != nil {
return err
}

ur.Ratings.Reported = true

return nil
}

// RemoveReport removes the report request from the review. Only works if the user
// has reported the review. See UserReview.Ratings.Reported if it is already reported.
func (ur *UserReview) RemoveReport() error {
if !ur.Ratings.Reported {
return fmt.Errorf("review is not reported")
}
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/user/%s/report/review/%s", ur.crunchy.Config.AccountID, ur.ReviewData.ID)
_, err := ur.crunchy.request(endpoint, http.MethodDelete)
if err != nil {
return err
}

ur.Ratings.Reported = false

return nil
}
123 changes: 123 additions & 0 deletions video.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,126 @@ func (s *Series) Seasons() (seasons []*Season, err error) {
}
return
}

// Rating returns the series rating.
func (s *Series) Rating() (*Rating, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/user/%s/rating/series/%s", s.crunchy.Config.AccountID, s.ID)
resp, err := s.crunchy.request(endpoint, http.MethodGet)
if err != nil {
return nil, err
}
defer resp.Body.Close()

rating := &Rating{}
json.NewDecoder(resp.Body).Decode(rating)

return rating, nil
}

// ReviewSortType represents a sort type to sort Series.Reviews items after.
type ReviewSortType string

const (
ReviewSortNewest ReviewSortType = "newest"
ReviewSortOldest = "oldest"
ReviewSortHelpful = "helpful"
)

// ReviewOptions represents options for fetching series reviews.
type ReviewOptions struct {
// Sort specifies how the items should be sorted.
Sort ReviewSortType `json:"sort"`
// Filter specified after which the returning items should be filtered.
Filter ReviewRating `json:"filter"`
}

// Reviews returns user reviews for the series.
func (s *Series) Reviews(options ReviewOptions, page uint, size uint) (BulkResult[*UserReview], error) {
options, err := structDefaults(ReviewOptions{Sort: ReviewSortNewest}, options)
if err != nil {
return BulkResult[*UserReview]{}, err
}
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/%s/user/%s/review/series/%s/list?page=%d&page_size=%d&sort=%s&filter=%s", s.crunchy.Locale, s.crunchy.Config.AccountID, s.ID, page, size, options.Sort, options.Filter)
resp, err := s.crunchy.request(endpoint, http.MethodGet)
if err != nil {
return BulkResult[*UserReview]{}, err
}
defer resp.Body.Close()

var result BulkResult[*UserReview]
json.NewDecoder(resp.Body).Decode(&result)

for _, review := range result.Items {
review.crunchy = s.crunchy
review.SeriesID = s.ID
}

return result, nil
}

// Rate rates the current series.
func (s *Series) Rate(rating ReviewRating) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/en-US/user/%s/review/series/%s", s.crunchy.Config.AccountID, s.ID)
body, _ := json.Marshal(map[string]string{"rating": string(rating)})
req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
_, err = s.crunchy.requestFull(req)
return err
}

// CreateReview creates a review for the current series with the logged-in account.
// Will fail if a review is already present. Check Series.HasOwnerReview if the account
// has already written a review. If this is the case, use Series.GetOwnerReview and user
// OwnerReview.Edit to edit the review.
func (s *Series) CreateReview(title, content string, spoiler bool) (*OwnerReview, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/en-US/user/%s/review/series/%s", s.crunchy.Config.AccountID, s.ID)
body, _ := json.Marshal(map[string]any{
"title": title,
"body": content,
"spoiler": spoiler,
})
req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
resp, err := s.crunchy.requestFull(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

review := &OwnerReview{}
json.NewDecoder(resp.Body).Decode(review)
review.crunchy = s.crunchy
review.SeriesID = s.ID

return review, nil
}

// GetOwnerReview returns the series review, written by the current logged-in account.
// Returns an error if no review was written yet.
func (s *Series) GetOwnerReview() (*OwnerReview, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content-reviews/v2/en-US/user/%s/review/series/%s", s.crunchy.Config.AccountID, s.ID)
resp, err := s.crunchy.request(endpoint, http.MethodGet)
if err != nil {
return nil, err
}
defer resp.Body.Close()

review := &OwnerReview{}
json.NewDecoder(resp.Body).Decode(review)
review.crunchy = s.crunchy
review.SeriesID = s.ID

return review, nil
}

// HasOwnerReview returns if the logged-in account has written a review for the series.
func (s *Series) HasOwnerReview() bool {
_, err := s.GetOwnerReview()
return err == nil
}