diff --git a/cmd/cmd_add.go b/cmd/cmd_add.go index 9d7bb15..c07f445 100644 --- a/cmd/cmd_add.go +++ b/cmd/cmd_add.go @@ -24,8 +24,8 @@ func handleMediaAdd(cmd *cobra.Command, args []string) { err = viewmodel.HandleMediaUpdate( viewmodel.MediaUpdateParams{ IsNewAddition: true, - MediaId: mediaId, - Status: mediaAddStatus, + MediaId: mediaId, + Status: mediaAddStatus, }, ) if err != nil { diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index c1018a0..cf6c036 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -22,7 +22,7 @@ var mediaListCmd = &cobra.Command{ Use: "list", Short: "List your current anime/manga list", Aliases: []string{"ls"}, - Run: handleLs, + Run: handleLs, } func init() { diff --git a/cmd/cmd_media_search.go b/cmd/cmd_media_search.go index db5c26c..17a224d 100644 --- a/cmd/cmd_media_search.go +++ b/cmd/cmd_media_search.go @@ -86,7 +86,7 @@ var mediaSearchCmd = &cobra.Command{ Use: "search [query...]", Short: "Search for anime and manga", Args: cobra.MinimumNArgs(1), - Run: handleMediaSearch, + Run: handleMediaSearch, } func init() { diff --git a/cmd/cmd_update.go b/cmd/cmd_update.go index 8a7e139..a746c33 100644 --- a/cmd/cmd_update.go +++ b/cmd/cmd_update.go @@ -11,6 +11,9 @@ import ( // // TODO: Update progress relatively. For example "+2", "-10" etc., var progress string +var updateStatus string +var notes string +var scoreString string // func handleUpdate(mediaId int) { // CheckIfTokenExists() @@ -48,15 +51,32 @@ func handleUpdate(cmd *cobra.Command, args []string) { ) } - err = viewmodel.HandleMediaUpdate( - viewmodel.MediaUpdateParams{ - IsNewAddition: false, - MediaId: id, - Progress: progress, - Status: "none", - StartDate: "none", - }, - ) + var scoreFloat *float64 + if scoreString != "" { + rawScoreFloat, err := strconv.ParseFloat(scoreString, 32) + if err != nil { + fmt.Println(ui.ErrorText(err)) + return + } + scoreFloat = &rawScoreFloat + } + + params := viewmodel.MediaUpdateParams{ + IsNewAddition: false, + MediaId: id, + Progress: progress, + Status: updateStatus, + StartDate: "none", + } + // TODO: add a way to differentiate between an + // empty notes value vs. an unset notes value + if notes != "\n" { + params.Notes = notes + } + if scoreFloat != nil { + params.Score = float32(*scoreFloat) + } + err = viewmodel.HandleMediaUpdate(params) if err != nil { fmt.Println(ui.ErrorText(err)) @@ -67,7 +87,7 @@ var mediaUpdateCmd = &cobra.Command{ Use: "update [id]", Short: "Update a list entry", Args: cobra.MinimumNArgs(1), - Run: handleUpdate, + Run: handleUpdate, } func init() { @@ -75,7 +95,20 @@ func init() { &progress, "progress", "p", - "0", + "", "The number of episodes/chapter to update", ) + mediaUpdateCmd.Flags().StringVarP( + &updateStatus, "status", "s", "none", "Status of the media. Can be 'watching/w or reading/r', 'planning/p', 'completed/c', 'dropped/d', 'paused/ps'", + ) + mediaUpdateCmd.Flags().StringVarP( + ¬es, + "notes", + "n", + "\n", + "Text notes. Note: you can add multiple lines by typing \"\\n\" and wrapping the note in double quotes", + ) + mediaUpdateCmd.Flags().StringVarP( + &scoreString, "score", "r", "", "The score of the entry. If your score is in emoji, type 1 for 😞, 2 for 😐 and 3 for 😊", + ) } diff --git a/internal/api/anilist.go b/internal/api/anilist.go index e4d30c5..443610c 100644 --- a/internal/api/anilist.go +++ b/internal/api/anilist.go @@ -145,4 +145,4 @@ func UpdateMediaEntry(params map[string]any) (*responses.MediaUpdateResponse, er } return &responseStruct, nil -} \ No newline at end of file +} diff --git a/internal/api/mutations.go b/internal/api/mutations.go index bfb52dd..499055a 100644 --- a/internal/api/mutations.go +++ b/internal/api/mutations.go @@ -37,4 +37,4 @@ const mediaEntryUpdateMutation = `mutation( } } } -}` \ No newline at end of file +}` diff --git a/internal/api/responses/media_list.go b/internal/api/responses/media_list.go index a9575e4..3eb75c0 100644 --- a/internal/api/responses/media_list.go +++ b/internal/api/responses/media_list.go @@ -11,9 +11,9 @@ type ListCollection struct { Title struct { UserPreferred string `json:"userPreferred"` } `json:"title"` - Chapters *int `json:"chapters"` - Volumes *int `json:"volumes"` - Episodes *int `json:"episodes"` + Chapters *int `json:"chapters"` + Volumes *int `json:"volumes"` + Episodes *int `json:"episodes"` MediaFormat string `json:"format"` } `json:"media"` } `json:"entries"` diff --git a/internal/api/responses/media_search.go b/internal/api/responses/media_search.go index 955c4c5..2329645 100644 --- a/internal/api/responses/media_search.go +++ b/internal/api/responses/media_search.go @@ -6,8 +6,8 @@ type MediaSearchList struct { UserPreferred string `json:"userPreferred"` } `json:"title"` AverageScore *float64 `json:"averageScore"` - MediaType string `json:"type"` - MediaFormat string `json:"format"` + MediaType string `json:"type"` + MediaFormat string `json:"format"` } type MediaSearch struct { diff --git a/internal/api/responses/media_update_response.go b/internal/api/responses/media_update_response.go index 8a0e4f1..c1f541e 100644 --- a/internal/api/responses/media_update_response.go +++ b/internal/api/responses/media_update_response.go @@ -4,11 +4,11 @@ type MediaUpdateResponse struct { Data struct { SaveMediaListEntry struct { Media struct { - Id int `json:"id"` + Id int `json:"id"` Title struct { UserPreferred string `json:"userPreferred"` } `json:"title"` } `json:"media"` } `json:"SaveMediaListEntry"` } `json:"data"` -} \ No newline at end of file +} diff --git a/internal/constants.go b/internal/constants.go index d140993..3c296da 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -4,16 +4,16 @@ package internal // Base URL is not gonna get changed for a while. // So, keeping it as constant is not gonna hurt anyone. -const( - API_ENDPOINT = "https://graphql.anilist.co" - AUTH_URL = "https://anilist.co/api/v2/oauth/authorize?client_id=4593&response_type=token" +const ( + API_ENDPOINT = "https://graphql.anilist.co" + AUTH_URL = "https://anilist.co/api/v2/oauth/authorize?client_id=4593&response_type=token" BOLT_BUCKET_NAME = "ChibiConfig" - BOLT_DB_NAME = "chibi_config.db" + BOLT_DB_NAME = "chibi_config.db" ) -type MediaType string +type MediaType string const ( ANIME MediaType = "ANIME" MANGA MediaType = "MANGA" -) \ No newline at end of file +) diff --git a/internal/db/queries.go b/internal/db/queries.go index 2cebe66..c257573 100644 --- a/internal/db/queries.go +++ b/internal/db/queries.go @@ -2,7 +2,7 @@ package db -const( +const ( QUERY_CREATE_TABLE = `CREATE TABLE IF NOT EXISTS config ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, @@ -12,4 +12,4 @@ const( QUERY_INSERT_CONFIG = `INSERT INTO config (key, value) VALUES (?, ?)` QUERY_GET_CONFIG = `SELECT value FROM config WHERE key = ?` -) \ No newline at end of file +) diff --git a/internal/helpers.go b/internal/helpers.go index 2c10e73..50b2f1b 100644 --- a/internal/helpers.go +++ b/internal/helpers.go @@ -17,7 +17,7 @@ func CreateConfigDir() { os.RemoveAll(configDir) } os.MkdirAll(configDir, 0755) -} +} // maps "type" command line argument string to valid // MediaType enum required by AniList API @@ -74,4 +74,4 @@ func MediaFormatFormatter(mediaFormat string) string { default: return "?" } -} \ No newline at end of file +} diff --git a/internal/ui/media_list.go b/internal/ui/media_list.go index 735853c..acc0ac3 100644 --- a/internal/ui/media_list.go +++ b/internal/ui/media_list.go @@ -4,7 +4,7 @@ import ( "fmt" "os" "strconv" - + "github.com/CosmicPredator/chibi/internal" "github.com/CosmicPredator/chibi/internal/api/responses" "github.com/charmbracelet/lipgloss" @@ -40,7 +40,7 @@ func (l *MediaListUI) renderTable(rows ...[]string) (*table.Table, error) { Align(lipgloss.Center) } - if row % 2 == 0 && (col == 0 || col == 2 || col == 3) { + if row%2 == 0 && (col == 0 || col == 2 || col == 3) { return lipgloss.NewStyle().Align(lipgloss.Center).Faint(true) } @@ -52,7 +52,7 @@ func (l *MediaListUI) renderTable(rows ...[]string) (*table.Table, error) { PaddingLeft(1). PaddingRight(1). Width((tw - 6) / 3).Inline(true) - if row % 2 == 0 { + if row%2 == 0 { colStyle = colStyle.Faint(true) } return colStyle @@ -103,7 +103,7 @@ func (l *MediaListUI) Render() error { if list.Status == "REPEATING" { entry.Media.Title.UserPreferred = "(R) " + entry.Media.Title.UserPreferred } - + rows = append(rows, []string{ strconv.Itoa(entry.Media.Id), entry.Media.Title.UserPreferred, @@ -121,4 +121,4 @@ func (l *MediaListUI) Render() error { fmt.Println(table) return nil -} \ No newline at end of file +} diff --git a/internal/ui/media_search.go b/internal/ui/media_search.go index a440e19..de7797a 100644 --- a/internal/ui/media_search.go +++ b/internal/ui/media_search.go @@ -32,13 +32,13 @@ func (ms *MediaSearchUI) renderTable(rows ...[]string) (*table.Table, error) { // style for table header row if row == -1 { return lipgloss. - NewStyle(). - Foreground(lipgloss.Color("#FF79C6")). - Bold(true). - Align(lipgloss.Center) + NewStyle(). + Foreground(lipgloss.Color("#FF79C6")). + Bold(true). + Align(lipgloss.Center) } - if row % 2 == 0 && (col == 0 || col == 2 || col == 3) { + if row%2 == 0 && (col == 0 || col == 2 || col == 3) { return lipgloss.NewStyle().Align(lipgloss.Center).Faint(true) } @@ -50,7 +50,7 @@ func (ms *MediaSearchUI) renderTable(rows ...[]string) (*table.Table, error) { PaddingLeft(2). PaddingRight(2). Width((tw - 6) / 3).Inline(true) - if row % 2 == 0 { + if row%2 == 0 { colStyle = colStyle.Faint(true) } return colStyle @@ -64,7 +64,7 @@ func (ms *MediaSearchUI) renderTable(rows ...[]string) (*table.Table, error) { }). Headers("ID", "TITLE", "FORMAT", "SCORE"). Rows(rows...).Width(tw) - + return table, nil } @@ -95,4 +95,4 @@ func (ms *MediaSearchUI) Render() error { fmt.Println(table) return nil -} \ No newline at end of file +} diff --git a/internal/ui/profile.go b/internal/ui/profile.go index df05a35..c129e9d 100644 --- a/internal/ui/profile.go +++ b/internal/ui/profile.go @@ -53,7 +53,7 @@ func (p *ProfileUI) Render() error { valueStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8BE9FD")) var sb strings.Builder - + // iterating over dataSlice and adding the KV pairs to String Builder // with appropriate padding for _, kv := range dataSlice { diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 7f08ce7..4db4b21 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -41,4 +41,4 @@ func ActionSpinner(title string, action func(context.Context) error) error { Title(title). ActionWithErr(action). Run() -} \ No newline at end of file +} diff --git a/internal/viewmodel/login_handler.go b/internal/viewmodel/login_handler.go index 091127a..b9635df 100644 --- a/internal/viewmodel/login_handler.go +++ b/internal/viewmodel/login_handler.go @@ -34,7 +34,7 @@ func HandleLogin() error { return err } - // gets user profile details from api and saves + // gets user profile details from api and saves // the username and ID to db var profile *responses.Profile err = ui.ActionSpinner("Logging In...", func(ctx context.Context) error { diff --git a/internal/viewmodel/media_list_handler.go b/internal/viewmodel/media_list_handler.go index 058c447..9769a7c 100644 --- a/internal/viewmodel/media_list_handler.go +++ b/internal/viewmodel/media_list_handler.go @@ -35,9 +35,9 @@ func HandleMediaList(mediaType, mediaStatus string) error { // current and repeating var mediaStatuIn []string if mediaStatus == "CURRENT" { - mediaStatuIn = []string{ mediaStatus, "REPEATING" } + mediaStatuIn = []string{mediaStatus, "REPEATING"} } else { - mediaStatuIn = []string{ mediaStatus } + mediaStatuIn = []string{mediaStatus} } // perform media list API request @@ -57,7 +57,7 @@ func HandleMediaList(mediaType, mediaStatus string) error { MediaType: mediaType, MediaList: mediaList, } - + err = mediaListUI.Render() return err -} \ No newline at end of file +} diff --git a/internal/viewmodel/media_search_handler.go b/internal/viewmodel/media_search_handler.go index a0d48a0..7db25fd 100644 --- a/internal/viewmodel/media_search_handler.go +++ b/internal/viewmodel/media_search_handler.go @@ -31,4 +31,4 @@ func HandleMediaSearch(searchQuery string, mediaType string, perPage int) error } err = mediaSearchUI.Render() return err -} \ No newline at end of file +} diff --git a/internal/viewmodel/media_update_handler.go b/internal/viewmodel/media_update_handler.go index 52a4711..f8d43ba 100644 --- a/internal/viewmodel/media_update_handler.go +++ b/internal/viewmodel/media_update_handler.go @@ -16,25 +16,26 @@ import ( "github.com/charmbracelet/huh/spinner" ) - type MediaUpdateParams struct { IsNewAddition bool - MediaId int - Progress string - Status string - StartDate string + MediaId int + Progress string + Status string + StartDate string + Notes string + Score float32 } // Gets current and total progress (episode/chapter) for given // Media ID and returns it func getCurrentProgress(userId int, mediaId int) (current int, total *int, err error) { var mediaList *responses.MediaList - + // load medialist collection err = spinner.New().Title("Getting your list...").Action(func() { mediaList, err = api.GetMediaList( userId, - []string{ "CURRENT", "REPEATING" }, + []string{"CURRENT", "REPEATING"}, ) }).Run() @@ -71,7 +72,7 @@ func getCurrentProgress(userId int, mediaId int) (current int, total *int, err e // this func gets incvoked when "chibi add" command is invoked func handleNewAdditionAction(params MediaUpdateParams) error { payload := map[string]any{ - "id": params.MediaId, + "id": params.MediaId, "status": internal.MediaStatusEnumMapper(params.Status), } @@ -83,11 +84,19 @@ func handleNewAdditionAction(params MediaUpdateParams) error { return err } - if params.Status == "CURRENT" { + if payload["status"] == "CURRENT" { payload["sDate"] = startDateRaw.Day() payload["sMonth"] = int(startDateRaw.Month()) payload["sYear"] = startDateRaw.Year() } + } else { + startDate := time.Now() + + if payload["status"] == "CURRENT" { + payload["sDate"] = startDate.Day() + payload["sMonth"] = int(startDate.Month()) + payload["sYear"] = startDate.Year() + } } // perform API mutate request @@ -112,7 +121,7 @@ func handleNewAdditionAction(params MediaUpdateParams) error { fmt.Println( ui.SuccessText( fmt.Sprintf( - "Added %s to %s", + "Added %s to %s", response.Data.SaveMediaListEntry.Media.Title.UserPreferred, statusString, ), @@ -194,19 +203,18 @@ func handleMediaCompletedAction(params MediaUpdateParams, progress int) error { // display success text fmt.Println( - ui.SuccessText( - fmt.Sprintf( - "Marked %s as completed", - response.Data.SaveMediaListEntry.Media.Title.UserPreferred), + ui.SuccessText( + fmt.Sprintf( + "Marked %s as completed", + response.Data.SaveMediaListEntry.Media.Title.UserPreferred), ), ) return nil } - // handles media update logic and functionalities -// This func has 3 scenarios/routes +// This func has 3 scenarios/routes // 1. Invoke handleNewAdditionAction() when MediaUpdateParams.IsNewAddition is true // 2. Invoke handleMediaCompletedAction() when current/accumulated progress == total progress // 3. else go on with the flow (just progress update) @@ -238,11 +246,26 @@ func HandleMediaUpdate(params MediaUpdateParams) error { return err } + status := internal.MediaStatusEnumMapper(params.Status) + if status == "COMPLETED" { + if *total != 0 && accumulatedProgress < *total { + var markAsCompleted string + fmt.Print("Accumulated progress is less than total episodes / chapters. Mark as media completed (y/N)? ") + fmt.Scan(&markAsCompleted) + + if strings.ToLower(markAsCompleted) != "y" { + return nil + } + } + err = handleMediaCompletedAction(params, accumulatedProgress) + return err + } + if total != nil { if *total != 0 && accumulatedProgress > *total { return fmt.Errorf("entered value is greater than total episodes / chapters, which is %d", *total) } - + // route 2 if accumulatedProgress == *total { var markAsCompleted string @@ -257,21 +280,32 @@ func HandleMediaUpdate(params MediaUpdateParams) error { } } + var notes string + if len(params.Notes) > 0 { + notes = strings.ReplaceAll(params.Notes, `\n`, "\n") + } + // route 3 var response *responses.MediaUpdateResponse err = ui.ActionSpinner("Updating entry...", func(ctx context.Context) error { - response, err = api.UpdateMediaEntry(map[string]any{ - "id": params.MediaId, + payload := map[string]any{ + "id": params.MediaId, "progress": accumulatedProgress, - }) + "status": status, + "notes": notes, + } + if params.Score > 0 { + payload["score"] = params.Score + } + response, err = api.UpdateMediaEntry(payload) return err }) fmt.Println( ui.SuccessText( fmt.Sprintf( - "Progress updated for %s (%d -> %d)\n", - response.Data.SaveMediaListEntry.Media.Title.UserPreferred, + "Progress updated for %s (%d -> %d)\n", + response.Data.SaveMediaListEntry.Media.Title.UserPreferred, current, accumulatedProgress), ), ) @@ -282,6 +316,9 @@ func HandleMediaUpdate(params MediaUpdateParams) error { // helper func to create absolute progress from relative progress func parseRelativeProgress(progress string, current int) (int, error) { var accumulatedProgress int + if len(progress) == 0 { + return current, nil + } if strings.Contains(progress, "+") || strings.Contains(progress, "-") { if progress[:1] == "+" { prgInt, _ := strconv.Atoi(progress[1:]) @@ -302,4 +339,4 @@ func parseRelativeProgress(progress string, current int) (int, error) { accumulatedProgress = pgrInt } return accumulatedProgress, nil -} \ No newline at end of file +}