mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Add comment endpoint
This commit is contained in:
parent
0c93893627
commit
cee3410532
3 changed files with 354 additions and 0 deletions
262
comment.go
Normal file
262
comment.go
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
package crunchyroll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Comment struct {
|
||||||
|
crunchy *Crunchyroll
|
||||||
|
|
||||||
|
EpisodeID string `json:"episode_id"`
|
||||||
|
|
||||||
|
CommentID string `json:"comment_id"`
|
||||||
|
DomainID string `json:"domain_id"`
|
||||||
|
|
||||||
|
GuestbookKey string `json:"guestbook_key"`
|
||||||
|
|
||||||
|
User struct {
|
||||||
|
UserKey string `json:"user_key"`
|
||||||
|
UserAttributes struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Avatar struct {
|
||||||
|
Locked []Image `json:"locked"`
|
||||||
|
Unlocked []Image `json:"unlocked"`
|
||||||
|
} `json:"avatar"`
|
||||||
|
} `json:"user_attributes"`
|
||||||
|
UserFlags []any `json:"user_flags"`
|
||||||
|
} `json:"user"`
|
||||||
|
|
||||||
|
Message string `json:"message"`
|
||||||
|
ParentCommentID int `json:"parent_comment_id"`
|
||||||
|
|
||||||
|
Locale LOCALE `json:"locale"`
|
||||||
|
|
||||||
|
UserVotes []string `json:"user_votes"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
|
Votes struct {
|
||||||
|
Inappropriate int `json:"inappropriate"`
|
||||||
|
Like int `json:"like"`
|
||||||
|
Spoiler int `json:"spoiler"`
|
||||||
|
} `json:"votes"`
|
||||||
|
|
||||||
|
DeleteReason any `json:"delete_reason"`
|
||||||
|
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Modified time.Time `json:"modified"`
|
||||||
|
|
||||||
|
IsOwner bool `json:"is_owner"`
|
||||||
|
RepliesCount int `json:"replies_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deleted the current comment. Works only if the user has written the comment.
|
||||||
|
func (c *Comment) Delete() error {
|
||||||
|
if !c.IsOwner {
|
||||||
|
return fmt.Errorf("cannot delete, user is not the comment author")
|
||||||
|
}
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
|
||||||
|
resp, err := c.crunchy.request(endpoint, http.MethodDelete)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// the api returns a new comment object when modifying it.
|
||||||
|
// hopefully this does not change
|
||||||
|
json.NewDecoder(resp.Body).Decode(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkAsSpoiler marks the current comment as spoiler. Works only if the user has written the comment,
|
||||||
|
// and it isn't already marked as spoiler.
|
||||||
|
func (c *Comment) MarkAsSpoiler() error {
|
||||||
|
if !c.IsOwner {
|
||||||
|
return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
|
||||||
|
} else if c.markedAs("spoiler") {
|
||||||
|
return fmt.Errorf("comment is already marked as spoiler")
|
||||||
|
}
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
|
||||||
|
body, _ := json.Marshal(map[string][]string{"add": {"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 := c.crunchy.requestFull(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarkAsSpoiler unmarks the current comment as spoiler. Works only if the user has written the comment,
|
||||||
|
// and it is already marked as spoiler.
|
||||||
|
func (c *Comment) UnmarkAsSpoiler() error {
|
||||||
|
if !c.IsOwner {
|
||||||
|
return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
|
||||||
|
} else if !c.markedAs("spoiler") {
|
||||||
|
return fmt.Errorf("comment is not marked as spoiler")
|
||||||
|
}
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
|
||||||
|
body, _ := json.Marshal(map[string][]string{"remove": {"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 := c.crunchy.requestFull(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like likes the comment. Works only if the user hasn't already liked it.
|
||||||
|
func (c *Comment) Like() error {
|
||||||
|
if err := c.vote("like", "liked"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Votes.Like += 1
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveLike removes the like from the comment. Works only if the user has liked it.
|
||||||
|
func (c *Comment) RemoveLike() error {
|
||||||
|
if err := c.unVote("like", "liked"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Votes.Like -= 1
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply replies to the current comment.
|
||||||
|
func (c *Comment) Reply(message string, spoiler bool) (*Comment, error) {
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?locale=%s", c.EpisodeID, c.crunchy.Locale)
|
||||||
|
var flags []string
|
||||||
|
if spoiler {
|
||||||
|
flags = append(flags, "spoiler")
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(map[string]any{"locale": string(c.crunchy.Locale), "message": message, "flags": flags, "parent_id": c.CommentID})
|
||||||
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
resp, err := c.crunchy.requestFull(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
reply := &Comment{}
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(reply); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replies shows all replies to the current comment.
|
||||||
|
func (c *Comment) Replies(page uint, size uint) ([]*Comment, error) {
|
||||||
|
if c.RepliesCount == 0 {
|
||||||
|
return []*Comment{}, nil
|
||||||
|
}
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/replies?page_size=%d&page=%d&locale=%s", c.EpisodeID, c.CommentID, size, page, c.Locale)
|
||||||
|
resp, err := c.crunchy.request(endpoint, http.MethodGet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var jsonBody map[string]any
|
||||||
|
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
||||||
|
|
||||||
|
var comments []*Comment
|
||||||
|
if err = decodeMapToStruct(jsonBody["items"].([]any), &comments); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return comments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report reports the comment. Only works if the comment hasn't been reported yet.
|
||||||
|
func (c *Comment) Report() error {
|
||||||
|
return c.vote("inappropriate", "reported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnreportComment removes the report request from the comment. Only works if the user
|
||||||
|
// has reported the comment.
|
||||||
|
func (c *Comment) UnreportComment() error {
|
||||||
|
return c.unVote("inappropriate", "reported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagAsSpoiler sends a request to the user (and / or crunchyroll?) to mark the comment
|
||||||
|
// as spoiler. Only works if the comment hasn't been flagged as spoiler yet.
|
||||||
|
func (c *Comment) FlagAsSpoiler() error {
|
||||||
|
return c.vote("spoiler", "spoiler")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnflagAsSpoiler rewokes the request to the user (and / or crunchyroll?) to mark the
|
||||||
|
// comment as spoiler. Only works if the user has flagged the comment as spoiler.
|
||||||
|
func (c *Comment) UnflagAsSpoiler() error {
|
||||||
|
return c.unVote("spoiler", "spoiler")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Comment) markedAs(voteType string) bool {
|
||||||
|
for _, userVote := range c.UserVotes {
|
||||||
|
if userVote == voteType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Comment) vote(voteType, readableName string) error {
|
||||||
|
if c.markedAs(voteType) {
|
||||||
|
return fmt.Errorf("comment is already marked as %s", readableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/votes?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
|
||||||
|
body, _ := json.Marshal(map[string]string{"vote_type": voteType})
|
||||||
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
_, err = c.crunchy.requestFull(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.UserVotes = append(c.UserVotes, voteType)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Comment) unVote(voteType, readableName string) error {
|
||||||
|
for i, userVote := range c.UserVotes {
|
||||||
|
if userVote == voteType {
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/votes?vote_type=%s&locale=%s", c.EpisodeID, c.CommentID, voteType, c.crunchy.Locale)
|
||||||
|
_, err := c.crunchy.request(endpoint, http.MethodDelete)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.UserVotes = append(c.UserVotes[:i], c.UserVotes[i+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("comment is not marked as %s", readableName)
|
||||||
|
}
|
||||||
80
episode.go
80
episode.go
|
|
@ -167,6 +167,86 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
|
||||||
return streams[0].AudioLocale, nil
|
return streams[0].AudioLocale, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comment creates a new comment under the episode.
|
||||||
|
func (e *Episode) Comment(message string, spoiler bool) (*Comment, error) {
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?locale=%s", e.ID, e.crunchy.Locale)
|
||||||
|
var flags []string
|
||||||
|
if spoiler {
|
||||||
|
flags = append(flags, "spoiler")
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(map[string]any{"locale": string(e.crunchy.Locale), "flags": flags, "message": message})
|
||||||
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
resp, err := e.crunchy.requestFull(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
c := &Comment{
|
||||||
|
crunchy: e.crunchy,
|
||||||
|
EpisodeID: e.ID,
|
||||||
|
}
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentsOrderType represents a sort type to sort Episode.Comments after.
|
||||||
|
type CommentsOrderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommentsOrderAsc CommentsOrderType = "asc"
|
||||||
|
CommentsOrderDesc = "desc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentsSortType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommentsSortPopular CommentsSortType = "popular"
|
||||||
|
CommentsSortDate = "date"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentsOptions struct {
|
||||||
|
// Order specified the order how the comments should be returned.
|
||||||
|
Order CommentsOrderType `json:"order"`
|
||||||
|
|
||||||
|
// Sort specified after which key the comments should be sorted.
|
||||||
|
Sort CommentsSortType `json:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comments returns comments under the given episode.
|
||||||
|
func (e *Episode) Comments(options CommentsOptions, page uint, size uint) (c []*Comment, err error) {
|
||||||
|
options, err = structDefaults(CommentsOptions{Order: CommentsOrderDesc, Sort: CommentsSortPopular}, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?page=%d&page_size=%d&order=%s&sort=%s&locale=%s", e.ID, page, size, options.Order, options.Sort, e.crunchy.Locale)
|
||||||
|
resp, err := e.crunchy.request(endpoint, http.MethodGet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var jsonBody map[string]any
|
||||||
|
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
||||||
|
|
||||||
|
if err = decodeMapToStruct(jsonBody["items"].([]any), &c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, comment := range c {
|
||||||
|
comment.crunchy = e.crunchy
|
||||||
|
comment.EpisodeID = e.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetFormat returns the format which matches the given resolution and subtitle locale.
|
// GetFormat returns the format which matches the given resolution and subtitle locale.
|
||||||
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
|
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
|
||||||
streams, err := e.Streams()
|
streams, err := e.Streams()
|
||||||
|
|
|
||||||
12
utils.go
12
utils.go
|
|
@ -1,6 +1,7 @@
|
||||||
package crunchyroll
|
package crunchyroll
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -70,3 +71,14 @@ func encodeStructToQueryValues(s interface{}) (string, error) {
|
||||||
|
|
||||||
return values.Encode(), nil
|
return values.Encode(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func structDefaults[T any](defaultStruct T, customStruct T) (T, error) {
|
||||||
|
rawDefaultStruct, err := json.Marshal(defaultStruct)
|
||||||
|
if err != nil {
|
||||||
|
return *new(T), err
|
||||||
|
}
|
||||||
|
if err = json.NewDecoder(bytes.NewBuffer(rawDefaultStruct)).Decode(&customStruct); err != nil {
|
||||||
|
return *new(T), err
|
||||||
|
}
|
||||||
|
return customStruct, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue