-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #69 from marcus-crane/new-playback
Introduce new playback system
- Loading branch information
Showing
40 changed files
with
1,596 additions
and
2,962 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,133 @@ | ||
package anilist | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"log/slog" | ||
"net/http" | ||
"time" | ||
"os" | ||
"strings" | ||
|
||
"github.com/marcus-crane/gunslinger/db" | ||
"github.com/marcus-crane/gunslinger/playback" | ||
"github.com/marcus-crane/gunslinger/utils" | ||
) | ||
|
||
const ( | ||
anilistGraphqlEndpoint = "https://graphql.anilist.co" | ||
) | ||
|
||
type Client struct { | ||
APIKey string | ||
BaseURL string | ||
HTTPClient *http.Client | ||
type AnilistResponse struct { | ||
Data AnilistData `json:"data"` | ||
} | ||
|
||
type AnilistData struct { | ||
Page Page `json:"Page"` | ||
} | ||
|
||
type Page struct { | ||
Activities []Activity `json:"activities"` | ||
} | ||
|
||
type Activity struct { | ||
Id int64 `json:"id"` | ||
Status string `json:"status"` | ||
Progress string `json:"progress"` | ||
CreatedAt int64 `json:"createdAt"` | ||
Media Manga `json:"media"` | ||
} | ||
|
||
type Manga struct { | ||
Id int64 `json:"id"` | ||
Title MangaTitle `json:"title"` | ||
Chapters int `json:"chapters"` | ||
CoverImage MangaCover `json:"coverImage"` | ||
} | ||
|
||
type MangaTitle struct { | ||
UserPreferred string `json:"userPreferred"` | ||
} | ||
|
||
type MangaCover struct { | ||
ExtraLarge string `json:"extraLarge"` | ||
} | ||
|
||
func NewClient(apiKey string) *Client { | ||
return &Client{ | ||
APIKey: apiKey, | ||
BaseURL: "https://graphql.anilist.co", | ||
HTTPClient: &http.Client{ | ||
Timeout: 10 * time.Second, | ||
}, | ||
func GetRecentlyReadManga(ps *playback.PlaybackSystem, store db.Store, client http.Client) { | ||
payload := strings.NewReader("{\"query\":\"query Test {\\n Page(page: 1, perPage: 10) {\\n activities(\\n\\t\\t\\tuserId: 6111545\\n type: MANGA_LIST\\n sort: ID_DESC\\n ) {\\n ... on ListActivity {\\n id\\n status\\n\\t\\t\\t\\tprogress\\n createdAt\\n media {\\n chapters\\n id\\n title {\\n userPreferred\\n }\\n coverImage {\\n extraLarge\\n }\\n }\\n }\\n }\\n }\\n}\\n\",\"variables\":{}}") | ||
req, err := http.NewRequest("POST", anilistGraphqlEndpoint, payload) | ||
if err != nil { | ||
slog.Error("Failed to build Anilist manga payload", slog.String("stack", err.Error())) | ||
return | ||
} | ||
req.Header = http.Header{ | ||
"Accept": []string{"application/json"}, | ||
"Authorization": []string{fmt.Sprintf("Bearer %s", os.Getenv("ANILIST_TOKEN"))}, | ||
"Content-Type": []string{"application/json"}, | ||
"User-Agent": []string{utils.UserAgent}, | ||
} | ||
res, err := client.Do(req) | ||
if err != nil { | ||
slog.Error("Failed to contact Anilist for manga updates", slog.String("stack", err.Error())) | ||
return | ||
} | ||
defer res.Body.Close() | ||
|
||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
slog.Error("Failed to read Anilist response", slog.String("stack", err.Error())) | ||
return | ||
} | ||
var anilistResponse AnilistResponse | ||
|
||
if err = json.Unmarshal(body, &anilistResponse); err != nil { | ||
slog.Error("Error fetching Anilist data", slog.String("stack", err.Error())) | ||
return | ||
} | ||
|
||
if len(anilistResponse.Data.Page.Activities) == 0 { | ||
slog.Warn("Found no activities for Anilist") | ||
} | ||
|
||
for _, activity := range anilistResponse.Data.Page.Activities { | ||
image, extension, domColours, err := utils.ExtractImageContent(activity.Media.CoverImage.ExtraLarge) | ||
if err != nil { | ||
slog.Error("Failed to extract image content", | ||
slog.String("stack", err.Error()), | ||
slog.String("image_url", activity.Media.CoverImage.ExtraLarge), | ||
) | ||
return | ||
} | ||
|
||
discImage, _ := utils.BytesToGUIDLocation(image, extension) | ||
|
||
update := playback.Update{ | ||
MediaItem: playback.MediaItem{ | ||
Title: activity.Progress, | ||
Subtitle: activity.Media.Title.UserPreferred, | ||
Category: string(playback.Manga), | ||
Duration: 0, | ||
Source: string(playback.Anilist), | ||
Image: discImage, | ||
DominantColours: domColours, | ||
}, | ||
Elapsed: 0, | ||
Status: playback.StatusStopped, | ||
} | ||
|
||
if err := ps.UpdatePlaybackState(update); err != nil { | ||
slog.Error("Failed to save Anilist update", | ||
slog.String("stack", err.Error()), | ||
slog.String("title", update.MediaItem.Title)) | ||
} | ||
|
||
hash := playback.GenerateMediaID(&update) | ||
if err := utils.SaveCover(hash, image, extension); err != nil { | ||
slog.Error("Failed to save cover for Anilist", | ||
slog.String("stack", err.Error()), | ||
slog.String("guid", hash), | ||
slog.String("title", update.MediaItem.Title), | ||
) | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Business Rules | ||
|
||
- Multiple sources can be active at the same time | ||
- ie; playing a game while listening to music | ||
- We treat paused items as being stopped but if playback is picked up, we mark the same instance as playing again (ie; podcasts listened to in chunks) | ||
- It is exceedingly rare that I would ever use two of the same media type at the same time ie; listen to two songs at once, play two games at once. | ||
- Different media types have different "priority" ie; a game and a song, the game is the main focus so would be highlighted as the main item with music as a background thing. | ||
- Support more than 2 sources but only surface highest priority two | ||
- Could be interesting to surface paused items if only one item active / recent ie; paused podcast slowly progressing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package main | ||
|
||
import ( | ||
"net/http" | ||
"time" | ||
|
||
"github.com/go-co-op/gocron" | ||
"github.com/marcus-crane/gunslinger/db" | ||
"github.com/marcus-crane/gunslinger/playback" | ||
"github.com/marcus-crane/gunslinger/plex" | ||
"github.com/marcus-crane/gunslinger/spotify" | ||
"github.com/marcus-crane/gunslinger/steam" | ||
"github.com/marcus-crane/gunslinger/trakt" | ||
"github.com/marcus-crane/gunslinger/utils" | ||
) | ||
|
||
var ( | ||
STORAGE_DIR = utils.GetEnv("STORAGE_DIR", "/tmp") | ||
) | ||
|
||
func SetupInBackground(ps *playback.PlaybackSystem, store db.Store) *gocron.Scheduler { | ||
s := gocron.NewScheduler(time.UTC) | ||
|
||
client := http.Client{} | ||
|
||
go spotify.SetupSpotifyPoller(ps, store) | ||
|
||
s.Every(1).Seconds().Do(plex.GetCurrentlyPlaying, ps, client) | ||
// s.Every(15).Seconds().Do(anilist.GetRecentlyReadManga, ps, store, client) // Rate limit: 90 req/sec | ||
s.Every(15).Seconds().Do(steam.GetCurrentlyPlaying, ps, client) | ||
s.Every(15).Seconds().Do(trakt.GetCurrentlyPlaying, ps, client) | ||
s.Every(15).Seconds().Do(trakt.GetCurrentlyListening, ps, client) | ||
|
||
// If we're redeployed, we'll populate the latest state | ||
ps.RefreshCurrentPlayback() | ||
|
||
return s | ||
} |
Oops, something went wrong.