Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle currently airing anime episodes correctly #37

Merged
merged 2 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,36 @@ curd

</details>

<details>
<summary>macOS Installation</summary>

Install required dependencies
```bash
brew install mpv curl
```

Download the appropriate binary for your system:

- For Apple Silicon (M1/M2) Macs:
```bash
curl -Lo curd https://github.com/Wraient/curd/releases/latest/download/curd-macos-arm64
```

- For Intel Macs:
```bash
curl -Lo curd https://github.com/Wraient/curd/releases/latest/download/curd-macos-x86_64
```

Then complete the installation:

```bash
chmod +x curd
sudo mv curd /usr/local/bin/
curd
```

</details>

<details>
<summary>Generic Installation</summary>

Expand Down Expand Up @@ -267,4 +297,4 @@ config file is located at ```~/.config/curd/curd.conf```

## Credits
- [ani-cli](https://github.com/pystardust/ani-cli) - Code for fetching anime url
- [jerry](https://github.com/justchokingaround/jerry) - For the inspiration
- [jerry](https://github.com/justchokingaround/jerry) - For the inspiration
30 changes: 19 additions & 11 deletions cmd/curd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"runtime"
"strconv"
"sync"
"time"

"github.com/wraient/curd/internal"
)

Expand Down Expand Up @@ -475,7 +476,7 @@ func main() {
}()

// Skip OP and ED and Save MPV Speed
skipLoop:
skipLoop:
for {
select {
case <-skipLoopDone:
Expand Down Expand Up @@ -520,17 +521,24 @@ func main() {
}()

anime.Ep.IsCompleted = false
// internal.CurdOut(anime.Ep.Number, anime.TotalEpisodes, &userCurdConfig)
if anime.Ep.Number-1 == anime.TotalEpisodes && userCurdConfig.ScoreOnCompletion {
anime.Ep.Number = anime.Ep.Number - 1
internal.CurdOut("Completed anime.")
err = internal.RateAnime(user.Token, anime.AnilistId)
// Only mark as complete and prompt for rating if we've reached the total episodes
// AND the anime is not currently airing (total episodes > 0)
if anime.Ep.Number-1 == anime.TotalEpisodes && userCurdConfig.ScoreOnCompletion && anime.TotalEpisodes > 0 {
// Get updated anime data to check if it's still airing
updatedAnime, err := internal.GetAnimeDataByID(anime.AnilistId, user.Token)
if err != nil {
internal.Log("Error rating anime: "+err.Error(), logFile)
internal.CurdOut("Error rating anime: " + err.Error())
internal.Log("Error getting updated anime data: "+err.Error(), logFile)
} else if !updatedAnime.IsAiring {
anime.Ep.Number = anime.Ep.Number - 1
internal.CurdOut("Completed anime.")
err = internal.RateAnime(user.Token, anime.AnilistId)
if err != nil {
internal.Log("Error rating anime: "+err.Error(), logFile)
internal.CurdOut("Error rating anime: " + err.Error())
}
internal.LocalDeleteAnime(databaseFile, anime.AnilistId, anime.AllanimeId)
internal.ExitCurd(nil)
}
internal.LocalDeleteAnime(databaseFile, anime.AnilistId, anime.AllanimeId)
internal.ExitCurd(nil)
}
}
if anime.Rewatching && anime.Ep.IsCompleted && anime.Ep.Number-1 == anime.TotalEpisodes {
Expand Down
105 changes: 44 additions & 61 deletions internal/anilist.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -33,7 +32,7 @@ func GetAnimeMap(animeList AnimeList) map[string]string {
for _, entry := range entries {
// Only include entries with a non-empty English title

if entry.Media.Title.English != "" && userCurdConfig.AnimeNameLanguage == "english" {
if entry.Media.Title.English != "" && userCurdConfig.AnimeNameLanguage == "english" {
animeMap[strconv.Itoa(entry.Media.ID)] = entry.Media.Title.English
} else {
animeMap[strconv.Itoa(entry.Media.ID)] = entry.Media.Title.Romaji
Expand Down Expand Up @@ -63,12 +62,12 @@ func GetAnimeMapPreview(animeList AnimeList) map[string]RofiSelectPreview {
Log(fmt.Sprintf("AnimeNameLanguage: ", userCurdConfig.AnimeNameLanguage), logFile)
if entry.Media.Title.English != "" && userCurdConfig.AnimeNameLanguage == "english" {
animeMap[strconv.Itoa(entry.Media.ID)] = RofiSelectPreview{
Title: entry.Media.Title.English,
Title: entry.Media.Title.English,
CoverImage: entry.CoverImage,
}
} else {
animeMap[strconv.Itoa(entry.Media.ID)] = RofiSelectPreview{
Title: entry.Media.Title.Romaji,
Title: entry.Media.Title.Romaji,
CoverImage: entry.CoverImage,
}
}
Expand Down Expand Up @@ -459,9 +458,9 @@ func SearchAnimeByTitle(jsonData map[string]interface{}, searchTitle string) []m

if strings.Contains(strings.ToLower(romajiTitle), strings.ToLower(searchTitle)) || strings.Contains(strings.ToLower(englishTitle), strings.ToLower(searchTitle)) {
result := map[string]interface{}{
"id": media["id"],
"progress": entry.(map[string]interface{})["progress"],
"romaji_title": romajiTitle,
"id": media["id"],
"progress": entry.(map[string]interface{})["progress"],
"romaji_title": romajiTitle,
"english_title": englishTitle,
"episodes": episodes,
"duration": duration,
Expand Down Expand Up @@ -609,7 +608,7 @@ func makePostRequest(url, query string, variables map[string]interface{}, header
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Content-Type", "application/json") // <-- Important!
req.Header.Set("Content-Type", "application/json") // <-- Important!
for key, value := range headers {
req.Header.Set(key, value)
}
Expand Down Expand Up @@ -691,9 +690,9 @@ func ParseAnimeList(input map[string]interface{}) AnimeList {
Episodes: toInt(media["episodes"]),
ID: toInt(media["id"]),
Title: AnimeTitle{
English: safeString(media["title"].(map[string]interface{})["english"]),
Romaji: safeString(media["title"].(map[string]interface{})["romaji"]),
Japanese: safeString(media["title"].(map[string]interface{})["native"]),
English: safeString(media["title"].(map[string]interface{})["english"]),
Romaji: safeString(media["title"].(map[string]interface{})["romaji"]),
Japanese: safeString(media["title"].(map[string]interface{})["native"]),
},
},
Progress: toInt(entryData["progress"]),
Expand Down Expand Up @@ -763,78 +762,62 @@ func FindAnimeByAnilistIDInAnimes(animes []Anime, anilistID int) (*Anime, error)
}

// GetAnimeDataByID retrieves detailed anime data from AniList using the anime's ID and user token
func GetAnimeDataByID(anilistID int, token string) (Anime, error) {
func GetAnimeDataByID(id int, token string) (Anime, error) {
url := "https://graphql.anilist.co"
query := `
query ($id: Int) {
Media (id: $id, type: ANIME) {
Media(id: $id, type: ANIME) {
id
title {
romaji
english
native
}
episodes
duration
status
nextAiringEpisode {
episode
}
}
}
`

}`

variables := map[string]interface{}{
"id": anilistID,
"id": id,
}

jsonValue, _ := json.Marshal(map[string]interface{}{
"query": query,
"variables": variables,
})
headers := map[string]string{
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
}

req, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(jsonValue))
response, err := makePostRequest(url, query, variables, headers)
if err != nil {
return Anime{}, fmt.Errorf("error creating request: %v", err)
return Anime{}, fmt.Errorf("failed to get anime data: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
data, ok := response["data"].(map[string]interface{})
if !ok {
return Anime{}, fmt.Errorf("invalid response format: data field missing")
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return Anime{}, fmt.Errorf("error sending request: %v", err)
media, ok := data["Media"].(map[string]interface{})
if !ok {
return Anime{}, fmt.Errorf("invalid response format: Media field missing")
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Anime{}, fmt.Errorf("error reading response: %v", err)
anime := Anime{
AnilistId: id,
IsAiring: false,
}

var result struct {
Data struct {
Media struct {
ID int `json:"id"`
Title AnimeTitle `json:"title"`
Episodes int `json:"episodes"`
Duration int `json:"duration"`
Status string `json:"status"`
CoverImage struct {
Large string `json:"large"`
} `json:"coverImage"`
Genres []string `json:"genres"`
AverageScore int `json:"averageScore"`
} `json:"Media"`
} `json:"data"`
// Safely handle episodes field which might be nil for currently airing shows
if episodes, ok := media["episodes"].(float64); ok {
anime.TotalEpisodes = int(episodes)
}

if err := json.Unmarshal(body, &result); err != nil {
return Anime{}, fmt.Errorf("error unmarshaling JSON: %v", err)
// Check status
if status, ok := media["status"].(string); ok {
anime.IsAiring = status == "RELEASING"
}

anime := Anime{
AnilistId: result.Data.Media.ID,
Title: result.Data.Media.Title,
TotalEpisodes: result.Data.Media.Episodes,
CoverImage: result.Data.Media.CoverImage.Large,
// Double check with nextAiringEpisode
if nextEp, ok := media["nextAiringEpisode"].(map[string]interface{}); ok && nextEp != nil {
anime.IsAiring = true
}

return anime, nil
Expand Down
25 changes: 13 additions & 12 deletions internal/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ type AnimeTitle struct {
}

type Anime struct {
Title AnimeTitle `json:"title"`
Ep Episode `json:"ep"`
CoverImage string `json:"url"` // Assuming this field corresponds to the cover image URL
TotalEpisodes int `json:"total_episodes"` // If provided by the API
MalId int `json:"mal_id"`
AnilistId int `json:"anilist_id"` // Assuming you have an Anilist ID in your struct
Rewatching bool
AllanimeId string // Can be populated as necessary
FillerEpisodes []int
Title AnimeTitle `json:"title"`
Ep Episode `json:"ep"`
CoverImage string `json:"url"` // Assuming this field corresponds to the cover image URL
TotalEpisodes int `json:"total_episodes"` // If provided by the API
MalId int `json:"mal_id"`
AnilistId int `json:"anilist_id"` // Assuming you have an Anilist ID in your struct
Rewatching bool
AllanimeId string // Can be populated as necessary
FillerEpisodes []int
IsAiring bool
}

type Skip struct {
Expand All @@ -37,7 +38,7 @@ type Episode struct {
Started bool `json:"started"`
Duration int `json:"duration"`
Links []string `json:"links"`
NextEpisode NextEpisode `json:"next_episode"`
NextEpisode NextEpisode `json:"next_episode"`
IsFiller bool `json:"filler"`
IsRecap bool `json:"recap"`
Aired string `json:"aired"`
Expand All @@ -48,8 +49,8 @@ type Episode struct {
}

type NextEpisode struct {
Number int
Links []string
Number int
Links []string
}

type playingVideo struct {
Expand Down