node build fixed
This commit is contained in:
1
seanime-2.9.10/internal/discordrpc/client/README.md
Normal file
1
seanime-2.9.10/internal/discordrpc/client/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Full credit to [thelennylord/discord-rpc](https://github.com/thelennylord/discord-rpc/)
|
||||
101
seanime-2.9.10/internal/discordrpc/client/activity.go
Normal file
101
seanime-2.9.10/internal/discordrpc/client/activity.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package discordrpc_client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Activity holds the data for discord rich presence
|
||||
//
|
||||
// See https://discord.com/developers/docs/game-sdk/activities#data-models-activity-struct
|
||||
type Activity struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
DetailsURL string `json:"details_url,omitempty"` // URL to details
|
||||
State string `json:"state,omitempty"`
|
||||
StateURL string `json:"state_url,omitempty"` // URL to state
|
||||
|
||||
Timestamps *Timestamps `json:"timestamps,omitempty"`
|
||||
Assets *Assets `json:"assets,omitempty"`
|
||||
Party *Party `json:"party,omitempty"`
|
||||
Secrets *Secrets `json:"secrets,omitempty"`
|
||||
Buttons []*Button `json:"buttons,omitempty"`
|
||||
|
||||
Instance bool `json:"instance"`
|
||||
Type int `json:"type"`
|
||||
StatusDisplayType int `json:"status_display_type,omitempty"` // 1 = name, 2 = details, 3 = state
|
||||
}
|
||||
|
||||
// Timestamps holds unix timestamps for start and/or end of the game
|
||||
//
|
||||
// See https://discord.com/developers/docs/game-sdk/activities#data-models-activitytimestamps-struct
|
||||
type Timestamps struct {
|
||||
Start *Epoch `json:"start,omitempty"`
|
||||
End *Epoch `json:"end,omitempty"`
|
||||
}
|
||||
|
||||
// Epoch wrapper around time.Time to ensure times are sent as a unix epoch int
|
||||
type Epoch struct{ time.Time }
|
||||
|
||||
// MarshalJSON converts time.Time to unix time int
|
||||
func (t Epoch) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.FormatInt(t.Unix(), 10)), nil
|
||||
}
|
||||
|
||||
// Assets passes image references for inclusion in rich presence
|
||||
//
|
||||
// See https://discord.com/developers/docs/game-sdk/activities#data-models-activityassets-struct
|
||||
type Assets struct {
|
||||
LargeImage string `json:"large_image,omitempty"`
|
||||
LargeText string `json:"large_text,omitempty"`
|
||||
LargeURL string `json:"large_url,omitempty"` // URL to large image, if any
|
||||
SmallImage string `json:"small_image,omitempty"`
|
||||
SmallText string `json:"small_text,omitempty"`
|
||||
SmallURL string `json:"small_url,omitempty"` // URL to small image, if any
|
||||
}
|
||||
|
||||
// Party holds information for the current party of the player
|
||||
type Party struct {
|
||||
ID string `json:"id"`
|
||||
Size []int `json:"size"` // seems to be element [0] is count and [1] is max
|
||||
}
|
||||
|
||||
// Secrets holds secrets for Rich Presence joining and spectating
|
||||
type Secrets struct {
|
||||
Join string `json:"join,omitempty"`
|
||||
Spectate string `json:"spectate,omitempty"`
|
||||
Match string `json:"match,omitempty"`
|
||||
}
|
||||
|
||||
type Button struct {
|
||||
Label string `json:"label,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// SetActivity sets the Rich Presence activity for the running application
|
||||
func (c *Client) SetActivity(activity Activity) error {
|
||||
payload := Payload{
|
||||
Cmd: SetActivityCommand,
|
||||
Args: Args{
|
||||
Pid: os.Getpid(),
|
||||
Activity: &activity,
|
||||
},
|
||||
Nonce: uuid.New(),
|
||||
}
|
||||
return c.SendPayload(payload)
|
||||
}
|
||||
|
||||
func (c *Client) CancelActivity() error {
|
||||
payload := Payload{
|
||||
Cmd: SetActivityCommand,
|
||||
Args: Args{
|
||||
Pid: os.Getpid(),
|
||||
Activity: nil,
|
||||
},
|
||||
Nonce: uuid.New(),
|
||||
}
|
||||
return c.SendPayload(payload)
|
||||
}
|
||||
55
seanime-2.9.10/internal/discordrpc/client/client.go
Normal file
55
seanime-2.9.10/internal/discordrpc/client/client.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package discordrpc_client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"seanime/internal/discordrpc/ipc"
|
||||
)
|
||||
|
||||
// Client wrapper for the Discord RPC client
|
||||
type Client struct {
|
||||
ClientID string
|
||||
Socket *discordrpc_ipc.Socket
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.Socket.Close()
|
||||
}
|
||||
|
||||
// New sends a handshake in the socket and returns an error or nil and an instance of Client
|
||||
func New(clientId string) (*Client, error) {
|
||||
if clientId == "" {
|
||||
return nil, fmt.Errorf("no clientId set")
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(handshake{"1", clientId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sock, err := discordrpc_ipc.NewConnection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client{Socket: sock, ClientID: clientId}
|
||||
|
||||
r, err := c.Socket.Send(0, string(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var responseBody Data
|
||||
if err := json.Unmarshal([]byte(r), &responseBody); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if responseBody.Code > 1000 {
|
||||
return nil, fmt.Errorf(responseBody.Message)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
49
seanime-2.9.10/internal/discordrpc/client/client_test.go
Normal file
49
seanime-2.9.10/internal/discordrpc/client/client_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package discordrpc_client
|
||||
|
||||
import (
|
||||
"seanime/internal/constants"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
drpc, err := New(constants.DiscordApplicationId)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect to discord ipc: %v", err)
|
||||
}
|
||||
defer drpc.Close()
|
||||
|
||||
mangaActivity := Activity{
|
||||
Details: "Boku no Kokoro no Yabai Yatsu",
|
||||
State: "Reading Chapter 30",
|
||||
Assets: &Assets{
|
||||
LargeImage: "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx101557-bEJu54cmVYxx.jpg",
|
||||
LargeText: "Boku no Kokoro no Yabai Yatsu",
|
||||
SmallImage: "logo",
|
||||
SmallText: "Seanime",
|
||||
},
|
||||
Timestamps: &Timestamps{
|
||||
Start: &Epoch{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
Instance: true,
|
||||
Type: 3,
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = drpc.SetActivity(mangaActivity)
|
||||
time.Sleep(10 * time.Second)
|
||||
mangaActivity2 := mangaActivity
|
||||
mangaActivity2.Timestamps.Start.Time = time.Now()
|
||||
mangaActivity2.State = "Reading Chapter 31"
|
||||
_ = drpc.SetActivity(mangaActivity2)
|
||||
return
|
||||
}()
|
||||
|
||||
//if err != nil {
|
||||
// t.Fatalf("failed to set activity: %v", err)
|
||||
//}
|
||||
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
114
seanime-2.9.10/internal/discordrpc/client/command.go
Normal file
114
seanime-2.9.10/internal/discordrpc/client/command.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package discordrpc_client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type command string
|
||||
|
||||
const (
|
||||
// DispatchCommand event dispatch
|
||||
DispatchCommand command = "DISPATCH"
|
||||
|
||||
// AuthorizeCommand used to authorize a new client with your app
|
||||
AuthorizeCommand command = "AUTHORIZE"
|
||||
|
||||
// AuthenticateCommand used to authenticate an existing client with your app
|
||||
AuthenticateCommand command = "AUTHENTICATE"
|
||||
|
||||
// GetGuildCommand used to retrieve guild information from the client
|
||||
GetGuildCommand command = "GET_GUILD"
|
||||
|
||||
// GetGuildsCommand used to retrieve a list of guilds from the client
|
||||
GetGuildsCommand command = "GET_GUILDS"
|
||||
|
||||
// GetChannelCommand used to retrieve channel information from the client
|
||||
GetChannelCommand command = "GET_CHANNEL"
|
||||
|
||||
// GetChannelsCommand used to retrieve a list of channels for a guild from the client
|
||||
GetChannelsCommand command = "GET_CHANNELS"
|
||||
|
||||
// SubscribeCommand used to subscribe to an RPC event
|
||||
SubscribeCommand command = "SUBSCRIBE"
|
||||
|
||||
// UnSubscribeCommand used to unsubscribe from an RPC event
|
||||
UnSubscribeCommand command = "UNSUBSCRIBE"
|
||||
|
||||
// SetUserVoiceSettingsCommand used to change voice settings of users in voice channels
|
||||
SetUserVoiceSettingsCommand command = "SET_USER_VOICE_SETTINGS"
|
||||
|
||||
// SelectVoiceChannelCommand used to join or leave a voice channel, group dm, or dm
|
||||
SelectVoiceChannelCommand command = "SELECT_VOICE_CHANNEL"
|
||||
|
||||
// GetSelectedVoiceChannelCommand used to get the current voice channel the client is in
|
||||
GetSelectedVoiceChannelCommand command = "GET_SELECTED_VOICE_CHANNEL"
|
||||
|
||||
// SelectTextChannelCommand used to join or leave a text channel, group dm, or dm
|
||||
SelectTextChannelCommand command = "SELECT_TEXT_CHANNEL"
|
||||
|
||||
// GetVoiceSettingsCommand used to retrieve the client's voice settings
|
||||
GetVoiceSettingsCommand command = "GET_VOICE_SETTINGS"
|
||||
|
||||
// SetVoiceSettingsCommand used to set the client's voice settings
|
||||
SetVoiceSettingsCommand command = "SET_VOICE_SETTINGS"
|
||||
|
||||
// CaptureShortcutCommand used to capture a keyboard shortcut entered by the user
|
||||
CaptureShortcutCommand command = "CAPTURE_SHORTCUT"
|
||||
|
||||
// SetCertifiedDevicesCommand used to send info about certified hardware devices
|
||||
SetCertifiedDevicesCommand command = "SET_CERTIFIED_DEVICES"
|
||||
|
||||
// SetActivityCommand used to update a user's Rich Presence
|
||||
SetActivityCommand command = "SET_ACTIVITY"
|
||||
|
||||
// SendActivityJoinInviteCommand used to consent to a Rich Presence Ask to Join request
|
||||
SendActivityJoinInviteCommand command = "SEND_ACTIVITY_JOIN_INVITE"
|
||||
|
||||
// CloseActivityRequestCommand used to reject a Rich Presence Ask to Join request
|
||||
CloseActivityRequestCommand command = "CLOSE_ACTIVITY_REQUEST"
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
Cmd command `json:"cmd"`
|
||||
Args Args `json:"args"`
|
||||
Event event `json:"evt,omitempty"`
|
||||
Data *Data `json:"data,omitempty"`
|
||||
Nonce uuid.UUID `json:"nonce"`
|
||||
}
|
||||
|
||||
// SendPayload sends payload to the Discord RPC server
|
||||
func (c *Client) SendPayload(payload Payload) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal the payload into JSON
|
||||
rb, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Send over the socket
|
||||
r, err := c.Socket.Send(1, string(rb))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Response usually matches the outgoing request, also a payload
|
||||
var responseBody Payload
|
||||
if err := json.Unmarshal([]byte(r), &responseBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Convert op codes to enums? Either way seems that 1000 is good, everything else is bad
|
||||
if responseBody.Data.Code > 1000 {
|
||||
return fmt.Errorf(responseBody.Data.Message)
|
||||
}
|
||||
|
||||
if responseBody.Nonce != payload.Nonce {
|
||||
return fmt.Errorf("invalid nonce")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
seanime-2.9.10/internal/discordrpc/client/events.go
Normal file
66
seanime-2.9.10/internal/discordrpc/client/events.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package discordrpc_client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ActivityEventData struct {
|
||||
Secret string `json:"secret"`
|
||||
User *User `json:"user"`
|
||||
}
|
||||
|
||||
type event string
|
||||
|
||||
var (
|
||||
ActivityJoinEvent event = "ACTIVITY_JOIN"
|
||||
ActivitySpectateEvent event = "ACTIVITY_SPECTATE"
|
||||
ActivityJoinRequestEvent event = "ACTIVITY_JOIN_REQUEST"
|
||||
)
|
||||
|
||||
func (c *Client) RegisterEvent(ch chan ActivityEventData, evt event) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload := Payload{
|
||||
Cmd: SubscribeCommand,
|
||||
Event: evt,
|
||||
Nonce: uuid.New(),
|
||||
}
|
||||
|
||||
err := c.SendPayload(payload)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
r, err := c.Socket.Read()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Event event `json:"event"`
|
||||
Data *ActivityEventData `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(r), &response); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if response.Event == evt {
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- *response.Data
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
25
seanime-2.9.10/internal/discordrpc/client/types.go
Normal file
25
seanime-2.9.10/internal/discordrpc/client/types.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package discordrpc_client
|
||||
|
||||
type handshake struct {
|
||||
V string `json:"v"`
|
||||
ClientID string `json:"client_id"`
|
||||
}
|
||||
|
||||
// Data section of the RPC response
|
||||
type Data struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Args seems to contain the most data, Pid here is mandatory
|
||||
type Args struct {
|
||||
Pid int `json:"pid"`
|
||||
Activity *Activity `json:"activity,omitempty"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Discriminator string `json:"discriminator"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
73
seanime-2.9.10/internal/discordrpc/ipc/ipc.go
Normal file
73
seanime-2.9.10/internal/discordrpc/ipc/ipc.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package discordrpc_ipc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GetIpcPath chooses the correct directory to the ipc socket and returns it
|
||||
func GetIpcPath() string {
|
||||
vn := []string{"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}
|
||||
|
||||
for _, name := range vn {
|
||||
path, exists := os.LookupEnv(name)
|
||||
|
||||
if exists {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return "/tmp"
|
||||
}
|
||||
|
||||
// Socket extends net.Conn methods
|
||||
type Socket struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
// Read the socket response
|
||||
func (socket *Socket) Read() (string, error) {
|
||||
buf := make([]byte, 512)
|
||||
payloadLength, err := socket.Conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
for i := 8; i < payloadLength; i++ {
|
||||
buffer.WriteByte(buf[i])
|
||||
}
|
||||
|
||||
r := buffer.String()
|
||||
if r == "" {
|
||||
return "", fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Send opcode and payload to the unix socket
|
||||
func (socket *Socket) Send(opcode int, payload string) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, int32(opcode))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = binary.Write(buf, binary.LittleEndian, int32(len(payload)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf.Write([]byte(payload))
|
||||
_, err = socket.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return socket.Read()
|
||||
}
|
||||
19
seanime-2.9.10/internal/discordrpc/ipc/ipc_notwin.go
Normal file
19
seanime-2.9.10/internal/discordrpc/ipc/ipc_notwin.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package discordrpc_ipc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewConnection opens the discord-ipc-0 unix socket
|
||||
func NewConnection() (*Socket, error) {
|
||||
sock, err := net.DialTimeout("unix", GetIpcPath()+"/discord-ipc-0", time.Second*2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Socket{sock}, nil
|
||||
}
|
||||
24
seanime-2.9.10/internal/discordrpc/ipc/ipc_windows.go
Normal file
24
seanime-2.9.10/internal/discordrpc/ipc/ipc_windows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package discordrpc_ipc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
)
|
||||
|
||||
// NewConnection opens the discord-ipc-0 named pipe
|
||||
func NewConnection() (*Socket, error) {
|
||||
// Connect to the Windows named pipe, this is a well known name
|
||||
// We use DialTimeout since it will block forever (or very, very long) on Windows
|
||||
// if the pipe is not available (Discord not running)
|
||||
t := 2 * time.Second
|
||||
sock, err := winio.DialPipe(`\\.\pipe\discord-ipc-0`, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Socket{sock}, nil
|
||||
}
|
||||
89
seanime-2.9.10/internal/discordrpc/presence/hook_events.go
Normal file
89
seanime-2.9.10/internal/discordrpc/presence/hook_events.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package discordrpc_presence
|
||||
|
||||
import (
|
||||
discordrpc_client "seanime/internal/discordrpc/client"
|
||||
"seanime/internal/hook_resolver"
|
||||
)
|
||||
|
||||
// DiscordPresenceAnimeActivityRequestedEvent is triggered when anime activity is requested, after the [animeActivity] is processed, and right before the activity is sent to queue.
|
||||
// There is no guarantee as to when or if the activity will be successfully sent to discord.
|
||||
// Note that this event is triggered every 6 seconds or so, avoid heavy processing or perform it only when the activity is changed.
|
||||
// Prevent default to stop the activity from being sent to discord.
|
||||
type DiscordPresenceAnimeActivityRequestedEvent struct {
|
||||
hook_resolver.Event
|
||||
// Anime activity object used to generate the activity
|
||||
AnimeActivity *AnimeActivity `json:"animeActivity"`
|
||||
|
||||
// Name of the activity
|
||||
Name string `json:"name"`
|
||||
// Details of the activity
|
||||
Details string `json:"details"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
// State of the activity
|
||||
State string `json:"state"`
|
||||
// Timestamps of the activity
|
||||
StartTimestamp *int64 `json:"startTimestamp"`
|
||||
EndTimestamp *int64 `json:"endTimestamp"`
|
||||
|
||||
// Assets of the activity
|
||||
LargeImage string `json:"largeImage"`
|
||||
LargeText string `json:"largeText"`
|
||||
LargeURL string `json:"largeUrl,omitempty"` // URL to large image, if any
|
||||
SmallImage string `json:"smallImage"`
|
||||
SmallText string `json:"smallText"`
|
||||
SmallURL string `json:"smallUrl,omitempty"` // URL to small image, if any
|
||||
|
||||
// Buttons of the activity
|
||||
Buttons []*discordrpc_client.Button `json:"buttons"`
|
||||
|
||||
// Whether the activity is an instance
|
||||
Instance bool `json:"instance"`
|
||||
// Type of the activity
|
||||
Type int `json:"type"`
|
||||
// StatusDisplayType controls formatting
|
||||
StatusDisplayType int `json:"statusDisplayType,omitempty"`
|
||||
}
|
||||
|
||||
// DiscordPresenceMangaActivityRequestedEvent is triggered when manga activity is requested, after the [mangaActivity] is processed, and right before the activity is sent to queue.
|
||||
// There is no guarantee as to when or if the activity will be successfully sent to discord.
|
||||
// Note that this event is triggered every 6 seconds or so, avoid heavy processing or perform it only when the activity is changed.
|
||||
// Prevent default to stop the activity from being sent to discord.
|
||||
type DiscordPresenceMangaActivityRequestedEvent struct {
|
||||
hook_resolver.Event
|
||||
// Manga activity object used to generate the activity
|
||||
MangaActivity *MangaActivity `json:"mangaActivity"`
|
||||
|
||||
// Name of the activity
|
||||
Name string `json:"name"`
|
||||
// Details of the activity
|
||||
Details string `json:"details"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
// State of the activity
|
||||
State string `json:"state"`
|
||||
// Timestamps of the activity
|
||||
StartTimestamp *int64 `json:"startTimestamp"`
|
||||
EndTimestamp *int64 `json:"endTimestamp"`
|
||||
|
||||
// Assets of the activity
|
||||
LargeImage string `json:"largeImage"`
|
||||
LargeText string `json:"largeText"`
|
||||
LargeURL string `json:"largeUrl,omitempty"` // URL to large image, if any
|
||||
SmallImage string `json:"smallImage"`
|
||||
SmallText string `json:"smallText"`
|
||||
SmallURL string `json:"smallUrl,omitempty"` // URL to small image, if any
|
||||
|
||||
// Buttons of the activity
|
||||
Buttons []*discordrpc_client.Button `json:"buttons"`
|
||||
|
||||
// Whether the activity is an instance
|
||||
Instance bool `json:"instance"`
|
||||
// Type of the activity
|
||||
Type int `json:"type"`
|
||||
// StatusDisplayType controls formatting
|
||||
StatusDisplayType int `json:"statusDisplayType,omitempty"`
|
||||
}
|
||||
|
||||
// DiscordPresenceClientClosedEvent is triggered when the discord rpc client is closed.
|
||||
type DiscordPresenceClientClosedEvent struct {
|
||||
hook_resolver.Event
|
||||
}
|
||||
667
seanime-2.9.10/internal/discordrpc/presence/presence.go
Normal file
667
seanime-2.9.10/internal/discordrpc/presence/presence.go
Normal file
@@ -0,0 +1,667 @@
|
||||
package discordrpc_presence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"seanime/internal/constants"
|
||||
"seanime/internal/database/models"
|
||||
discordrpc_client "seanime/internal/discordrpc/client"
|
||||
"seanime/internal/hook"
|
||||
"seanime/internal/util"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Presence struct {
|
||||
client *discordrpc_client.Client
|
||||
settings *models.DiscordSettings
|
||||
logger *zerolog.Logger
|
||||
hasSent bool
|
||||
username string
|
||||
mu sync.RWMutex
|
||||
|
||||
animeActivity *AnimeActivity
|
||||
lastAnimeActivityUpdateSent time.Time
|
||||
|
||||
lastSent time.Time
|
||||
eventQueue chan func()
|
||||
cancelFunc context.CancelFunc // Cancel function for the event loop context
|
||||
}
|
||||
|
||||
// New creates a new Presence instance.
|
||||
// If rich presence is enabled, it sets up a new discord rpc client.
|
||||
func New(settings *models.DiscordSettings, logger *zerolog.Logger) *Presence {
|
||||
var client *discordrpc_client.Client
|
||||
|
||||
if settings != nil && settings.EnableRichPresence {
|
||||
var err error
|
||||
client, err = discordrpc_client.New(constants.DiscordApplicationId)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("discordrpc: rich presence enabled but failed to create discord rpc client")
|
||||
}
|
||||
}
|
||||
|
||||
p := &Presence{
|
||||
client: client,
|
||||
settings: settings,
|
||||
logger: logger,
|
||||
lastAnimeActivityUpdateSent: time.Now().Add(5 * time.Second),
|
||||
lastSent: time.Now().Add(-5 * time.Second),
|
||||
hasSent: false,
|
||||
eventQueue: make(chan func(), 100),
|
||||
}
|
||||
|
||||
if settings != nil && settings.EnableRichPresence {
|
||||
p.startEventLoop()
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Presence) startEventLoop() {
|
||||
// Cancel any existing goroutine
|
||||
if p.cancelFunc != nil {
|
||||
p.cancelFunc()
|
||||
}
|
||||
|
||||
// Create new context with cancel
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.cancelFunc = cancel
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
p.logger.Debug().Msg("discordrpc: Event loop stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
select {
|
||||
case job := <-p.eventQueue:
|
||||
p.mu.RLock()
|
||||
if p.client == nil {
|
||||
p.mu.RUnlock()
|
||||
continue
|
||||
}
|
||||
job()
|
||||
p.lastSent = time.Now()
|
||||
p.mu.RUnlock()
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close closes the discord rpc client.
|
||||
// If the client is nil, it does nothing.
|
||||
func (p *Presence) Close() {
|
||||
p.close()
|
||||
p.animeActivity = nil
|
||||
}
|
||||
|
||||
func (p *Presence) close() {
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/Close", func() {})
|
||||
p.clearEventQueue()
|
||||
|
||||
// Cancel the event loop goroutine
|
||||
if p.cancelFunc != nil {
|
||||
p.cancelFunc()
|
||||
p.cancelFunc = nil
|
||||
}
|
||||
|
||||
if p.client == nil {
|
||||
return
|
||||
}
|
||||
p.client.Close()
|
||||
p.client = nil
|
||||
|
||||
_ = hook.GlobalHookManager.OnDiscordPresenceClientClosed().Trigger(&DiscordPresenceClientClosedEvent{})
|
||||
}
|
||||
|
||||
func (p *Presence) SetSettings(settings *models.DiscordSettings) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/SetSettings", func() {})
|
||||
|
||||
// Close the current client and stop event loop
|
||||
p.Close()
|
||||
|
||||
settings.RichPresenceUseMediaTitleStatus = false // Devnote: Not used anymore, disable
|
||||
settings.RichPresenceShowAniListMediaButton = false // Devnote: Not used anymore, disable
|
||||
p.settings = settings
|
||||
|
||||
// Create a new client if rich presence is enabled
|
||||
if settings.EnableRichPresence {
|
||||
p.logger.Info().Msg("discordrpc: Discord Rich Presence enabled")
|
||||
p.setClient()
|
||||
} else {
|
||||
p.client = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Presence) SetUsername(username string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.username = username
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (p *Presence) setClient() {
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/setClient", func() {})
|
||||
|
||||
if p.client == nil {
|
||||
client, err := discordrpc_client.New(constants.DiscordApplicationId)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("discordrpc: Rich presence enabled but failed to create discord rpc client")
|
||||
return
|
||||
}
|
||||
p.client = client
|
||||
p.startEventLoop()
|
||||
p.logger.Debug().Msg("discordrpc: RPC client initialized and event loop started")
|
||||
}
|
||||
}
|
||||
|
||||
var isChecking bool
|
||||
|
||||
// check executes multiple checks to determine if the presence should be set.
|
||||
// It returns true if the presence should be set.
|
||||
func (p *Presence) check() (proceed bool) {
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/check", func() {
|
||||
proceed = false
|
||||
})
|
||||
|
||||
if isChecking {
|
||||
return false
|
||||
}
|
||||
isChecking = true
|
||||
defer func() {
|
||||
isChecking = false
|
||||
}()
|
||||
|
||||
// If the client is nil, return false
|
||||
if p.settings == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If rich presence is disabled, return false
|
||||
if !p.settings.EnableRichPresence {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the client is nil, create a new client
|
||||
if p.client == nil {
|
||||
p.setClient()
|
||||
}
|
||||
|
||||
// If the client is still nil, return false
|
||||
if p.client == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If this is the first time setting the presence, return true
|
||||
if !p.hasSent {
|
||||
p.hasSent = true
|
||||
return true
|
||||
}
|
||||
|
||||
// // If the last sent time is less than 5 seconds ago, return false
|
||||
// if time.Since(p.lastSent) < 5*time.Second {
|
||||
// rest := 5*time.Second - time.Since(p.lastSent)
|
||||
// time.Sleep(rest)
|
||||
// }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
defaultActivity = discordrpc_client.Activity{
|
||||
Name: "Seanime",
|
||||
Details: "",
|
||||
State: "",
|
||||
Assets: &discordrpc_client.Assets{
|
||||
LargeImage: "",
|
||||
LargeText: "",
|
||||
SmallImage: "https://seanime.app/images/circular-logo.png",
|
||||
SmallText: "Seanime v" + constants.Version,
|
||||
SmallURL: "https://seanime.app",
|
||||
},
|
||||
Timestamps: &discordrpc_client.Timestamps{
|
||||
Start: &discordrpc_client.Epoch{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
Buttons: []*discordrpc_client.Button{
|
||||
{
|
||||
Label: "Seanime",
|
||||
Url: "https://seanime.app",
|
||||
},
|
||||
},
|
||||
Instance: true,
|
||||
Type: 3,
|
||||
StatusDisplayType: 2,
|
||||
}
|
||||
)
|
||||
|
||||
func isSeanimeButtonPresent(activity *discordrpc_client.Activity) bool {
|
||||
if activity == nil || activity.Buttons == nil {
|
||||
return false
|
||||
}
|
||||
for _, button := range activity.Buttons {
|
||||
if button.Label == "Seanime" && button.Url == "https://seanime.app" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AnimeActivity struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Image string `json:"image"`
|
||||
IsMovie bool `json:"isMovie"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Paused bool `json:"paused"`
|
||||
Progress int `json:"progress"`
|
||||
Duration int `json:"duration"`
|
||||
TotalEpisodes *int `json:"totalEpisodes,omitempty"`
|
||||
CurrentEpisodeCount *int `json:"currentEpisodeCount,omitempty"`
|
||||
EpisodeTitle *string `json:"episodeTitle,omitempty"`
|
||||
}
|
||||
|
||||
func animeActivityKey(a *AnimeActivity) string {
|
||||
return fmt.Sprintf("%d:%d", a.ID, a.EpisodeNumber)
|
||||
}
|
||||
|
||||
func (p *Presence) SetAnimeActivity(a *AnimeActivity) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/SetAnimeActivity", func() {})
|
||||
|
||||
if !p.check() {
|
||||
return
|
||||
}
|
||||
|
||||
if !p.settings.EnableAnimeRichPresence {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear the queue if the anime activity is different
|
||||
if p.animeActivity != nil && animeActivityKey(a) != animeActivityKey(p.animeActivity) {
|
||||
p.clearEventQueue()
|
||||
}
|
||||
|
||||
event := &DiscordPresenceAnimeActivityRequestedEvent{}
|
||||
|
||||
state := fmt.Sprintf("Watching Episode %d", a.EpisodeNumber)
|
||||
//if a.TotalEpisodes != nil {
|
||||
// state += fmt.Sprintf(" of %d", *a.TotalEpisodes)
|
||||
//}
|
||||
if a.IsMovie {
|
||||
state = "Watching Movie"
|
||||
}
|
||||
|
||||
activity := defaultActivity
|
||||
activity.Details = a.Title
|
||||
activity.DetailsURL = fmt.Sprintf("https://anilist.co/anime/%d", a.ID)
|
||||
activity.State = state
|
||||
activity.Assets.LargeImage = a.Image
|
||||
activity.Assets.LargeText = a.Title
|
||||
activity.Assets.LargeURL = fmt.Sprintf("https://anilist.co/anime/%d", a.ID)
|
||||
|
||||
// Calculate the start time
|
||||
startTime := time.Now()
|
||||
if a.Progress > 0 {
|
||||
startTime = startTime.Add(-time.Duration(a.Progress) * time.Second)
|
||||
}
|
||||
|
||||
activity.Timestamps.Start.Time = startTime
|
||||
event.StartTimestamp = lo.ToPtr(startTime.Unix())
|
||||
endTime := startTime.Add(time.Duration(a.Duration) * time.Second)
|
||||
activity.Timestamps.End = &discordrpc_client.Epoch{
|
||||
Time: endTime,
|
||||
}
|
||||
event.EndTimestamp = lo.ToPtr(endTime.Unix())
|
||||
|
||||
// Hide the end timestamp if the anime is paused
|
||||
if a.Paused {
|
||||
activity.Timestamps.End = nil
|
||||
event.EndTimestamp = nil
|
||||
}
|
||||
|
||||
activity.Buttons = make([]*discordrpc_client.Button, 0)
|
||||
|
||||
if p.settings.RichPresenceShowAniListProfileButton {
|
||||
activity.Buttons = append(activity.Buttons, &discordrpc_client.Button{
|
||||
Label: "View Profile",
|
||||
Url: fmt.Sprintf("https://anilist.co/user/%s", p.username),
|
||||
})
|
||||
}
|
||||
|
||||
if !(p.settings.RichPresenceHideSeanimeRepositoryButton || len(activity.Buttons) > 1) {
|
||||
activity.Buttons = append(activity.Buttons, &discordrpc_client.Button{
|
||||
Label: "Seanime",
|
||||
Url: "https://seanime.app",
|
||||
})
|
||||
}
|
||||
|
||||
// p.logger.Debug().Msgf("discordrpc: Setting anime activity: %s", a.Title)
|
||||
|
||||
p.animeActivity = a
|
||||
|
||||
event.AnimeActivity = a
|
||||
event.Name = activity.Name
|
||||
event.Details = activity.Details
|
||||
event.DetailsURL = activity.DetailsURL
|
||||
event.State = state
|
||||
event.LargeImage = activity.Assets.LargeImage
|
||||
event.LargeText = activity.Assets.LargeText
|
||||
event.LargeURL = activity.Assets.LargeURL
|
||||
event.SmallImage = activity.Assets.SmallImage
|
||||
event.SmallText = activity.Assets.SmallText
|
||||
event.SmallURL = activity.Assets.SmallURL
|
||||
event.Buttons = activity.Buttons
|
||||
event.Instance = defaultActivity.Instance
|
||||
event.Type = defaultActivity.Type
|
||||
|
||||
_ = hook.GlobalHookManager.OnDiscordPresenceAnimeActivityRequested().Trigger(event)
|
||||
|
||||
if event.DefaultPrevented {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the activity
|
||||
activity.Name = event.Name
|
||||
activity.Details = event.Details
|
||||
activity.DetailsURL = event.DetailsURL
|
||||
activity.State = event.State
|
||||
activity.Assets.LargeImage = event.LargeImage
|
||||
activity.Assets.LargeText = event.LargeText
|
||||
activity.Assets.LargeURL = event.LargeURL
|
||||
activity.Buttons = event.Buttons
|
||||
// Only allow changing small image and text if Seanime button is present
|
||||
if isSeanimeButtonPresent(&activity) {
|
||||
activity.Assets.SmallImage = event.SmallImage
|
||||
activity.Assets.SmallText = event.SmallText
|
||||
activity.Assets.SmallURL = event.SmallURL
|
||||
}
|
||||
// Update start timestamp
|
||||
if event.StartTimestamp != nil {
|
||||
activity.Timestamps.Start.Time = time.Unix(*event.StartTimestamp, 0)
|
||||
} else {
|
||||
activity.Timestamps.Start = nil
|
||||
}
|
||||
// Update end timestamp
|
||||
if event.EndTimestamp != nil {
|
||||
activity.Timestamps.End = &discordrpc_client.Epoch{
|
||||
Time: time.Unix(*event.EndTimestamp, 0),
|
||||
}
|
||||
} else {
|
||||
activity.Timestamps.End = nil
|
||||
}
|
||||
// Reset timestamps if both are nil
|
||||
if event.StartTimestamp == nil && event.EndTimestamp == nil {
|
||||
activity.Timestamps = nil
|
||||
}
|
||||
activity.Instance = event.Instance
|
||||
activity.Type = event.Type
|
||||
|
||||
select {
|
||||
case p.eventQueue <- func() {
|
||||
_ = p.client.SetActivity(activity)
|
||||
// p.logger.Debug().Int("progress", a.Progress).Int("duration", a.Duration).Msgf("discordrpc: Anime activity set for %s", a.Title)
|
||||
}:
|
||||
default:
|
||||
//p.logger.Error().Msgf("discordrpc: event queue is full for %s", a.Title)
|
||||
}
|
||||
}
|
||||
|
||||
// clearEventQueue drains the event queue channel
|
||||
func (p *Presence) clearEventQueue() {
|
||||
//p.logger.Debug().Msg("discordrpc: Clearing event queue")
|
||||
for {
|
||||
select {
|
||||
case <-p.eventQueue:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Presence) UpdateAnimeActivity(progress int, duration int, paused bool) {
|
||||
// do not lock, we call SetAnimeActivity
|
||||
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/UpdateWatching", func() {})
|
||||
|
||||
if p.animeActivity == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.animeActivity.Progress = progress
|
||||
p.animeActivity.Duration = duration
|
||||
|
||||
// Pause status changed
|
||||
if p.animeActivity.Paused != paused {
|
||||
// p.logger.Debug().Msgf("discordrpc: Pause status changed to %t for %s", paused, p.animeActivity.Title)
|
||||
p.animeActivity.Paused = paused
|
||||
p.lastAnimeActivityUpdateSent = time.Now()
|
||||
|
||||
// Clear the event queue to ensure pause/unpause takes precedence
|
||||
p.clearEventQueue()
|
||||
|
||||
if paused {
|
||||
// p.logger.Debug().Msgf("discordrpc: Stopping activity for %s", p.animeActivity.Title)
|
||||
// Stop the current activity if paused
|
||||
// but do not erase the current activity
|
||||
// p.close()
|
||||
|
||||
// edit: just switch to default timestamp
|
||||
p.SetAnimeActivity(p.animeActivity)
|
||||
} else {
|
||||
// p.logger.Debug().Msgf("discordrpc: Restarting activity for %s", p.animeActivity.Title)
|
||||
// Restart the current activity if unpaused
|
||||
p.SetAnimeActivity(p.animeActivity)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handles seeking
|
||||
if !p.animeActivity.Paused {
|
||||
// If the last update was more than 5 seconds ago, update the activity
|
||||
if time.Since(p.lastAnimeActivityUpdateSent) > 6*time.Second {
|
||||
// p.logger.Debug().Msgf("discordrpc: Updating activity for %s", p.animeActivity.Title)
|
||||
p.lastAnimeActivityUpdateSent = time.Now()
|
||||
p.SetAnimeActivity(p.animeActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type LegacyAnimeActivity struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Image string `json:"image"`
|
||||
IsMovie bool `json:"isMovie"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
}
|
||||
|
||||
// LegacySetAnimeActivity sets the presence to watching anime.
|
||||
func (p *Presence) LegacySetAnimeActivity(a *LegacyAnimeActivity) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/SetAnimeActivity", func() {})
|
||||
|
||||
if !p.check() {
|
||||
return
|
||||
}
|
||||
|
||||
if !p.settings.EnableAnimeRichPresence {
|
||||
return
|
||||
}
|
||||
|
||||
state := fmt.Sprintf("Watching Episode %d", a.EpisodeNumber)
|
||||
if a.IsMovie {
|
||||
state = "Watching Movie"
|
||||
}
|
||||
|
||||
activity := defaultActivity
|
||||
activity.Details = a.Title
|
||||
activity.DetailsURL = fmt.Sprintf("https://anilist.co/anime/%d", a.ID)
|
||||
activity.State = state
|
||||
activity.Assets.LargeImage = a.Image
|
||||
activity.Assets.LargeText = a.Title
|
||||
activity.Assets.LargeURL = fmt.Sprintf("https://anilist.co/anime/%d", a.ID)
|
||||
activity.Timestamps.Start.Time = time.Now()
|
||||
activity.Timestamps.End = nil
|
||||
activity.Buttons = make([]*discordrpc_client.Button, 0)
|
||||
|
||||
if p.settings.RichPresenceShowAniListProfileButton {
|
||||
activity.Buttons = append(activity.Buttons, &discordrpc_client.Button{
|
||||
Label: "View Profile",
|
||||
Url: fmt.Sprintf("https://anilist.co/user/%s", p.username),
|
||||
})
|
||||
}
|
||||
|
||||
if !(p.settings.RichPresenceHideSeanimeRepositoryButton || len(activity.Buttons) > 1) {
|
||||
activity.Buttons = append(activity.Buttons, &discordrpc_client.Button{
|
||||
Label: "Seanime",
|
||||
Url: "https://seanime.app",
|
||||
})
|
||||
}
|
||||
|
||||
// p.logger.Debug().Msgf("discordrpc: Setting anime activity: %s", a.Title)
|
||||
|
||||
p.eventQueue <- func() {
|
||||
_ = p.client.SetActivity(activity)
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type MangaActivity struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Image string `json:"image"`
|
||||
Chapter string `json:"chapter"`
|
||||
}
|
||||
|
||||
// SetMangaActivity sets the presence to watching anime.
|
||||
func (p *Presence) SetMangaActivity(a *MangaActivity) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
defer util.HandlePanicInModuleThen("discordrpc/presence/SetMangaActivity", func() {})
|
||||
|
||||
if !p.check() {
|
||||
return
|
||||
}
|
||||
|
||||
if !p.settings.EnableMangaRichPresence {
|
||||
return
|
||||
}
|
||||
|
||||
event := &DiscordPresenceMangaActivityRequestedEvent{}
|
||||
|
||||
activity := defaultActivity
|
||||
activity.Details = a.Title
|
||||
activity.DetailsURL = fmt.Sprintf("https://anilist.co/manga/%d", a.ID)
|
||||
activity.State = fmt.Sprintf("Reading Chapter %s", a.Chapter)
|
||||
activity.Assets.LargeImage = a.Image
|
||||
activity.Assets.LargeText = a.Title
|
||||
activity.Assets.LargeURL = fmt.Sprintf("https://anilist.co/manga/%d", a.ID)
|
||||
|
||||
now := time.Now()
|
||||
activity.Timestamps.Start.Time = now
|
||||
event.StartTimestamp = lo.ToPtr(now.Unix())
|
||||
activity.Timestamps.End = nil
|
||||
event.EndTimestamp = nil
|
||||
activity.Buttons = make([]*discordrpc_client.Button, 0)
|
||||
|
||||
if p.settings.RichPresenceShowAniListProfileButton && p.username != "" {
|
||||
activity.Buttons = append(activity.Buttons, &discordrpc_client.Button{
|
||||
Label: "View Profile",
|
||||
Url: fmt.Sprintf("https://anilist.co/user/%s", p.username),
|
||||
})
|
||||
}
|
||||
|
||||
if !(p.settings.RichPresenceHideSeanimeRepositoryButton || len(activity.Buttons) > 1) {
|
||||
activity.Buttons = append(activity.Buttons, &discordrpc_client.Button{
|
||||
Label: "Seanime",
|
||||
Url: "https://seanime.app",
|
||||
})
|
||||
}
|
||||
|
||||
event.MangaActivity = a
|
||||
event.Name = activity.Name
|
||||
event.Details = activity.Details
|
||||
event.DetailsURL = activity.DetailsURL
|
||||
event.State = activity.State
|
||||
event.LargeImage = activity.Assets.LargeImage
|
||||
event.LargeText = activity.Assets.LargeText
|
||||
event.LargeURL = activity.Assets.LargeURL
|
||||
event.SmallImage = activity.Assets.SmallImage
|
||||
event.SmallText = activity.Assets.SmallText
|
||||
event.SmallURL = activity.Assets.SmallURL
|
||||
event.Buttons = activity.Buttons
|
||||
event.Instance = activity.Instance
|
||||
event.Type = activity.Type
|
||||
|
||||
_ = hook.GlobalHookManager.OnDiscordPresenceMangaActivityRequested().Trigger(event)
|
||||
|
||||
if event.DefaultPrevented {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the activity
|
||||
activity.Name = event.Name
|
||||
activity.Details = event.Details
|
||||
activity.DetailsURL = event.DetailsURL
|
||||
activity.State = event.State
|
||||
activity.Assets.LargeImage = event.LargeImage
|
||||
activity.Assets.LargeText = event.LargeText
|
||||
activity.Assets.LargeURL = event.LargeURL
|
||||
activity.Buttons = event.Buttons
|
||||
// Only allow changing small image and text if Seanime button is present
|
||||
if isSeanimeButtonPresent(&activity) {
|
||||
activity.Assets.SmallImage = event.SmallImage
|
||||
activity.Assets.SmallText = event.SmallText
|
||||
activity.Assets.SmallURL = event.SmallURL
|
||||
}
|
||||
activity.Instance = event.Instance
|
||||
activity.Type = event.Type
|
||||
// Update start timestamp
|
||||
if event.StartTimestamp != nil {
|
||||
activity.Timestamps.Start.Time = time.Unix(*event.StartTimestamp, 0)
|
||||
} else {
|
||||
activity.Timestamps.Start = nil
|
||||
}
|
||||
// Update end timestamp
|
||||
if event.EndTimestamp != nil {
|
||||
activity.Timestamps.End = &discordrpc_client.Epoch{
|
||||
Time: time.Unix(*event.EndTimestamp, 0),
|
||||
}
|
||||
} else {
|
||||
activity.Timestamps.End = nil
|
||||
}
|
||||
// Reset timestamps if both are nil
|
||||
if event.StartTimestamp == nil && event.EndTimestamp == nil {
|
||||
activity.Timestamps = nil
|
||||
}
|
||||
|
||||
p.logger.Debug().Msgf("discordrpc: Setting manga activity: %s", a.Title)
|
||||
|
||||
p.eventQueue <- func() {
|
||||
_ = p.client.SetActivity(activity)
|
||||
}
|
||||
}
|
||||
60
seanime-2.9.10/internal/discordrpc/presence/presence_test.go
Normal file
60
seanime-2.9.10/internal/discordrpc/presence/presence_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package discordrpc_presence
|
||||
|
||||
import (
|
||||
"seanime/internal/database/models"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPresence(t *testing.T) {
|
||||
|
||||
settings := &models.DiscordSettings{
|
||||
EnableRichPresence: true,
|
||||
EnableAnimeRichPresence: true,
|
||||
EnableMangaRichPresence: true,
|
||||
}
|
||||
|
||||
presence := New(nil, util.NewLogger())
|
||||
presence.SetSettings(settings)
|
||||
presence.SetUsername("test")
|
||||
defer presence.Close()
|
||||
|
||||
presence.SetMangaActivity(&MangaActivity{
|
||||
Title: "Boku no Kokoro no Yabai Yatsu",
|
||||
Image: "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx101557-bEJu54cmVYxx.jpg",
|
||||
Chapter: "30",
|
||||
})
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
// Simulate settings being updated
|
||||
|
||||
settings.EnableMangaRichPresence = false
|
||||
presence.SetSettings(settings)
|
||||
presence.SetUsername("test")
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
presence.SetMangaActivity(&MangaActivity{
|
||||
Title: "Boku no Kokoro no Yabai Yatsu",
|
||||
Image: "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx101557-bEJu54cmVYxx.jpg",
|
||||
Chapter: "31",
|
||||
})
|
||||
|
||||
// Simulate settings being updated
|
||||
|
||||
settings.EnableMangaRichPresence = true
|
||||
presence.SetSettings(settings)
|
||||
presence.SetUsername("test")
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
presence.SetMangaActivity(&MangaActivity{
|
||||
Title: "Boku no Kokoro no Yabai Yatsu",
|
||||
Image: "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx101557-bEJu54cmVYxx.jpg",
|
||||
Chapter: "31",
|
||||
})
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
Reference in New Issue
Block a user