mirror of
https://github.com/5rahim/seanime
synced 2026-04-25 22:34:56 +02:00
feat(plugins): VideoCore bindings
This commit is contained in:
@@ -36421,7 +36421,7 @@
|
||||
"typescriptType": "string",
|
||||
"declaredValues": [
|
||||
"\"native-player\"",
|
||||
"\"video-core\"",
|
||||
"\"videocore\"",
|
||||
"\"nakama\"",
|
||||
"\"plugin\"",
|
||||
"\"playlist\""
|
||||
@@ -40077,10 +40077,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -64951,9 +64951,7 @@
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" PeerID of the sender"
|
||||
]
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Username",
|
||||
@@ -64962,9 +64960,7 @@
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" Display name of sender"
|
||||
]
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Message",
|
||||
@@ -64973,9 +64969,7 @@
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" Chat message content"
|
||||
]
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Timestamp",
|
||||
@@ -64985,9 +64979,7 @@
|
||||
"usedStructName": "time.Time",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" When the message was sent"
|
||||
]
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "MessageId",
|
||||
@@ -64997,7 +64989,7 @@
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" Unique message ID"
|
||||
" Unique id"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -68200,10 +68192,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -68756,10 +68748,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -68808,10 +68800,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -69190,10 +69182,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -69301,10 +69293,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -69547,10 +69539,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -69676,10 +69668,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -69752,10 +69744,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -70658,10 +70650,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": [
|
||||
@@ -73507,10 +73499,10 @@
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -73601,10 +73593,10 @@
|
||||
{
|
||||
"name": "Scheduler",
|
||||
"jsonName": "Scheduler",
|
||||
"goType": "goja_util.Scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "goja_util.Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
@@ -73666,6 +73658,161 @@
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/plugin/videocore.go",
|
||||
"filename": "videocore.go",
|
||||
"name": "VideoCore",
|
||||
"formattedName": "VideoCore",
|
||||
"package": "plugin",
|
||||
"fields": [
|
||||
{
|
||||
"name": "ctx",
|
||||
"jsonName": "ctx",
|
||||
"goType": "AppContextImpl",
|
||||
"typescriptType": "AppContextImpl",
|
||||
"usedTypescriptType": "AppContextImpl",
|
||||
"usedStructName": "plugin.AppContextImpl",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "vm",
|
||||
"jsonName": "vm",
|
||||
"goType": "goja.Runtime",
|
||||
"typescriptType": "Runtime",
|
||||
"usedTypescriptType": "Runtime",
|
||||
"usedStructName": "goja.Runtime",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "logger",
|
||||
"jsonName": "logger",
|
||||
"goType": "zerolog.Logger",
|
||||
"typescriptType": "Logger",
|
||||
"usedTypescriptType": "Logger",
|
||||
"usedStructName": "zerolog.Logger",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "ext",
|
||||
"jsonName": "ext",
|
||||
"goType": "extension.Extension",
|
||||
"typescriptType": "Extension_Extension",
|
||||
"usedTypescriptType": "Extension_Extension",
|
||||
"usedStructName": "extension.Extension",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "scheduler",
|
||||
"jsonName": "scheduler",
|
||||
"goType": "gojautil.Scheduler",
|
||||
"typescriptType": "Scheduler",
|
||||
"usedTypescriptType": "Scheduler",
|
||||
"usedStructName": "gojautil.Scheduler",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "listeners",
|
||||
"jsonName": "listeners",
|
||||
"goType": "",
|
||||
"typescriptType": "any",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "videoCoreSubscriber",
|
||||
"jsonName": "videoCoreSubscriber",
|
||||
"goType": "videocore.Subscriber",
|
||||
"typescriptType": "VideoCore_Subscriber",
|
||||
"usedTypescriptType": "VideoCore_Subscriber",
|
||||
"usedStructName": "videocore.Subscriber",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "unsubscribeOnce",
|
||||
"jsonName": "unsubscribeOnce",
|
||||
"goType": "sync.Once",
|
||||
"typescriptType": "Once",
|
||||
"usedTypescriptType": "Once",
|
||||
"usedStructName": "sync.Once",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/plugin/videocore.go",
|
||||
"filename": "videocore.go",
|
||||
"name": "VideoCoreEventListener",
|
||||
"formattedName": "VideoCoreEventListener",
|
||||
"package": "plugin",
|
||||
"fields": [
|
||||
{
|
||||
"name": "eventId",
|
||||
"jsonName": "eventId",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "listenerCh",
|
||||
"jsonName": "listenerCh",
|
||||
"goType": "",
|
||||
"typescriptType": "any",
|
||||
"required": true,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "closed",
|
||||
"jsonName": "closed",
|
||||
"goType": "atomic.Bool",
|
||||
"typescriptType": "Bool",
|
||||
"usedTypescriptType": "Bool",
|
||||
"usedStructName": "atomic.Bool",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "closeOnce",
|
||||
"jsonName": "closeOnce",
|
||||
"goType": "sync.Once",
|
||||
"typescriptType": "Once",
|
||||
"usedTypescriptType": "Once",
|
||||
"usedStructName": "sync.Once",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/plugin/videocore.go",
|
||||
"filename": "videocore.go",
|
||||
"name": "VideoCoreEvent",
|
||||
"formattedName": "VideoCoreEvent",
|
||||
"package": "plugin",
|
||||
"fields": [],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/report/report.go",
|
||||
"filename": "report.go",
|
||||
@@ -82873,7 +83020,7 @@
|
||||
"filename": "scheduler.go",
|
||||
"name": "Job",
|
||||
"formattedName": "Job",
|
||||
"package": "goja_util",
|
||||
"package": "gojautil",
|
||||
"fields": [
|
||||
{
|
||||
"name": "fn",
|
||||
@@ -82914,7 +83061,7 @@
|
||||
"filename": "scheduler.go",
|
||||
"name": "Scheduler",
|
||||
"formattedName": "Scheduler",
|
||||
"package": "goja_util",
|
||||
"package": "gojautil",
|
||||
"fields": [
|
||||
{
|
||||
"name": "jobQueue",
|
||||
@@ -82964,7 +83111,7 @@
|
||||
"goType": "Job",
|
||||
"typescriptType": "Job",
|
||||
"usedTypescriptType": "Job",
|
||||
"usedStructName": "goja_util.Job",
|
||||
"usedStructName": "gojautil.Job",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
@@ -84339,7 +84486,9 @@
|
||||
"\"video-error\"",
|
||||
"\"video-terminated\"",
|
||||
"\"video-playback-state\"",
|
||||
"\"subtitle-file-uploaded\""
|
||||
"\"subtitle-file-uploaded\"",
|
||||
"\"video-playlist\"",
|
||||
"\"video-text-tracks\""
|
||||
]
|
||||
},
|
||||
"comments": []
|
||||
@@ -84469,6 +84618,54 @@
|
||||
" VideoSubtitleTrack is an external subtitle track."
|
||||
]
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
"name": "VideoTextTrack",
|
||||
"formattedName": "VideoCore_VideoTextTrack",
|
||||
"package": "videocore",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Number",
|
||||
"jsonName": "number",
|
||||
"goType": "int",
|
||||
"typescriptType": "number",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
"jsonName": "type",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" \"subtitles\" | \"captions\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Label",
|
||||
"jsonName": "label",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Language",
|
||||
"jsonName": "language",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
@@ -84801,6 +84998,84 @@
|
||||
" It is filled by the client, passed to the player and sent to the server during playback."
|
||||
]
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
"name": "VideoPlaylistState",
|
||||
"formattedName": "VideoCore_VideoPlaylistState",
|
||||
"package": "videocore",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Type",
|
||||
"jsonName": "type",
|
||||
"goType": "PlaybackType",
|
||||
"typescriptType": "VideoCore_PlaybackType",
|
||||
"usedTypescriptType": "VideoCore_PlaybackType",
|
||||
"usedStructName": "videocore.PlaybackType",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Episodes",
|
||||
"jsonName": "episodes",
|
||||
"goType": "[]anime.Episode",
|
||||
"typescriptType": "Array\u003cAnime_Episode\u003e",
|
||||
"usedTypescriptType": "Anime_Episode",
|
||||
"usedStructName": "anime.Episode",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "PreviousEpisode",
|
||||
"jsonName": "previousEpisode",
|
||||
"goType": "anime.Episode",
|
||||
"typescriptType": "Anime_Episode",
|
||||
"usedTypescriptType": "Anime_Episode",
|
||||
"usedStructName": "anime.Episode",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "NextEpisode",
|
||||
"jsonName": "nextEpisode",
|
||||
"goType": "anime.Episode",
|
||||
"typescriptType": "Anime_Episode",
|
||||
"usedTypescriptType": "Anime_Episode",
|
||||
"usedStructName": "anime.Episode",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "CurrentEpisode",
|
||||
"jsonName": "currentEpisode",
|
||||
"goType": "anime.Episode",
|
||||
"typescriptType": "Anime_Episode",
|
||||
"usedTypescriptType": "Anime_Episode",
|
||||
"usedStructName": "anime.Episode",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "AnimeEntry",
|
||||
"jsonName": "animeEntry",
|
||||
"goType": "anime.Entry",
|
||||
"typescriptType": "Anime_Entry",
|
||||
"usedTypescriptType": "Anime_Entry",
|
||||
"usedStructName": "anime.Entry",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": [
|
||||
" VideoPlaylistState holds the state for the video player's playlist and playback."
|
||||
]
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
@@ -85574,6 +85849,54 @@
|
||||
"videocore.BaseVideoEvent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
"name": "VideoPlaylistEvent",
|
||||
"formattedName": "VideoCore_VideoPlaylistEvent",
|
||||
"package": "videocore",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Playlist",
|
||||
"jsonName": "playlist",
|
||||
"goType": "VideoPlaylistState",
|
||||
"typescriptType": "VideoCore_VideoPlaylistState",
|
||||
"usedTypescriptType": "VideoCore_VideoPlaylistState",
|
||||
"usedStructName": "videocore.VideoPlaylistState",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": [],
|
||||
"embeddedStructNames": [
|
||||
"videocore.BaseVideoEvent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
"name": "VideoTextTracksEvent",
|
||||
"formattedName": "VideoCore_VideoTextTracksEvent",
|
||||
"package": "videocore",
|
||||
"fields": [
|
||||
{
|
||||
"name": "TextTracks",
|
||||
"jsonName": "textTracks",
|
||||
"goType": "[]VideoTextTrack",
|
||||
"typescriptType": "Array\u003cVideoCore_VideoTextTrack\u003e",
|
||||
"usedTypescriptType": "VideoCore_VideoTextTrack",
|
||||
"usedStructName": "videocore.VideoTextTrack",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": [],
|
||||
"embeddedStructNames": [
|
||||
"videocore.BaseVideoEvent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/videocore/types.go",
|
||||
"filename": "types.go",
|
||||
@@ -85600,13 +85923,17 @@
|
||||
"\"terminate\"",
|
||||
"\"start-onlinestream-watch-party\"",
|
||||
"\"get-status\"",
|
||||
"\"show-message\"",
|
||||
"\"play-episode\"",
|
||||
"\"get-text-tracks\"",
|
||||
"\"get-fullscreen\"",
|
||||
"\"get-pip\"",
|
||||
"\"get-anime-4k\"",
|
||||
"\"get-subtitle-track\"",
|
||||
"\"get-audio-track\"",
|
||||
"\"get-media-caption-track\"",
|
||||
"\"get-playback-state\""
|
||||
"\"get-playback-state\"",
|
||||
"\"get-playlist\""
|
||||
]
|
||||
},
|
||||
"comments": []
|
||||
@@ -85792,6 +86119,28 @@
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "settingsMu",
|
||||
"jsonName": "settingsMu",
|
||||
"goType": "sync.RWMutex",
|
||||
"typescriptType": "RWMutex",
|
||||
"usedTypescriptType": "RWMutex",
|
||||
"usedStructName": "sync.RWMutex",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "settings",
|
||||
"jsonName": "settings",
|
||||
"goType": "models.Settings",
|
||||
"typescriptType": "Models_Settings",
|
||||
"usedTypescriptType": "Models_Settings",
|
||||
"usedStructName": "models.Settings",
|
||||
"required": false,
|
||||
"public": false,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
@@ -85803,6 +86152,15 @@
|
||||
"formattedName": "VideoCore_Subscriber",
|
||||
"package": "videocore",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"jsonName": "id",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": true,
|
||||
"public": false,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "eventCh",
|
||||
"jsonName": "eventCh",
|
||||
|
||||
@@ -2,6 +2,7 @@ package metadata_provider
|
||||
|
||||
import (
|
||||
"seanime/internal/database/db"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/filecache"
|
||||
"testing"
|
||||
@@ -13,8 +14,9 @@ func GetMockProvider(t *testing.T, db *db.Database) Provider {
|
||||
filecacher, err := filecache.NewCacher(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
return NewProvider(&NewProviderImplOptions{
|
||||
Logger: util.NewLogger(),
|
||||
FileCacher: filecacher,
|
||||
Database: db,
|
||||
Logger: util.NewLogger(),
|
||||
FileCacher: filecacher,
|
||||
Database: db,
|
||||
ExtensionBankRef: util.NewRef(extension.NewUnifiedBank()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -412,6 +412,10 @@ func (a *App) InitOrRefreshModules() {
|
||||
}
|
||||
}
|
||||
|
||||
if a.VideoCore != nil {
|
||||
a.VideoCore.SetSettings(settings)
|
||||
}
|
||||
|
||||
if settings.MediaPlayer != nil {
|
||||
a.MediaPlayer.VLC = &vlc.VLC{
|
||||
Host: settings.MediaPlayer.Host,
|
||||
|
||||
@@ -4,7 +4,7 @@ type WebsocketClientEventType string
|
||||
|
||||
const (
|
||||
NativePlayerEventType WebsocketClientEventType = "native-player"
|
||||
VideoCoreEventType WebsocketClientEventType = "video-core"
|
||||
VideoCoreEventType WebsocketClientEventType = "videocore"
|
||||
NakamaEventType WebsocketClientEventType = "nakama"
|
||||
PluginEvent WebsocketClientEventType = "plugin"
|
||||
PlaylistEvent WebsocketClientEventType = "playlist"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/goja/goja_runtime"
|
||||
"seanime/internal/plugin"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
@@ -24,7 +24,7 @@ type gojaProviderBase struct {
|
||||
source string
|
||||
runtimeManager *goja_runtime.Manager
|
||||
store *plugin.Store[string, any]
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
wsEventManager events.WSEventManagerInterface
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func initializeProviderBase(
|
||||
source: source,
|
||||
runtimeManager: runtimeManager,
|
||||
store: plugin.NewStore[string, any](nil), // Create a store (must be stopped when unloading)
|
||||
scheduler: goja_util.NewScheduler(), // Create a scheduler (must be stopped when unloading)
|
||||
scheduler: gojautil.NewScheduler(), // Create a scheduler (must be stopped when unloading)
|
||||
wsEventManager: wsEventManager,
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func initializeProviderBase(
|
||||
providerBase.store.Bind(vm, providerBase.scheduler)
|
||||
// Bind the shared bindings
|
||||
ShareBinds(vm, logger, ext, wsEventManager)
|
||||
goja_util.BindMutable(vm)
|
||||
gojautil.BindMutable(vm)
|
||||
BindUserConfig(vm, ext, logger)
|
||||
return vm
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"seanime/internal/plugin"
|
||||
plugin_ui "seanime/internal/plugin/ui"
|
||||
"seanime/internal/util"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -55,7 +55,7 @@ type GojaPlugin struct {
|
||||
store *plugin.Store[string, any]
|
||||
storage *plugin.Storage
|
||||
ui *plugin_ui.UI
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
loader *goja.Runtime
|
||||
unbindHookFuncs []func()
|
||||
interrupted bool
|
||||
@@ -132,7 +132,7 @@ func NewGojaPlugin(
|
||||
logger: logger,
|
||||
runtimeManager: runtimeManager,
|
||||
store: plugin.NewStore[string, any](nil), // Create a store (must be stopped when unloading)
|
||||
scheduler: goja_util.NewScheduler(), // Create a scheduler (must be stopped when unloading)
|
||||
scheduler: gojautil.NewScheduler(), // Create a scheduler (must be stopped when unloading)
|
||||
ui: nil, // To be initialized
|
||||
loader: goja.New(), // To be initialized
|
||||
unbindHookFuncs: []func(){},
|
||||
@@ -235,9 +235,9 @@ func (p *GojaPlugin) BindPluginAPIs(vm *goja.Runtime, logger *zerolog.Logger) {
|
||||
// Bind the store
|
||||
p.store.Bind(vm, p.scheduler)
|
||||
// Bind mutable bindings
|
||||
goja_util.BindMutable(vm)
|
||||
gojautil.BindMutable(vm)
|
||||
// Bind await bindings
|
||||
goja_util.BindAwait(vm)
|
||||
gojautil.BindAwait(vm)
|
||||
// Bind console bindings
|
||||
_ = goja_bindings.BindConsoleWithWS(p.ext, vm, logger, p.wsEventManager)
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ func InitTestPlugin(t testing.TB, opts TestPluginOptions) (*GojaPlugin, *zerolog
|
||||
PlaybackManager: &playbackmanager.PlaybackManager{},
|
||||
})
|
||||
|
||||
plugin, _, err := NewGojaPlugin(ext, opts.Language, logger, manager, wsEventManager)
|
||||
return plugin, logger, manager, anilistPlatform, wsEventManager, err
|
||||
p, _, err := NewGojaPlugin(ext, opts.Language, logger, manager, wsEventManager)
|
||||
return p, logger, manager, anilistPlatform, wsEventManager, err
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -167,6 +167,11 @@ declare namespace $ui {
|
||||
* Use a headless browser.
|
||||
*/
|
||||
chromeDP: ChromeDP
|
||||
|
||||
/**
|
||||
* Video Core for controlling the built-in player
|
||||
*/
|
||||
videoCore: VideoCore
|
||||
}
|
||||
|
||||
interface State<T> {
|
||||
@@ -218,6 +223,7 @@ declare namespace $ui {
|
||||
contentType: string
|
||||
/** Response content length */
|
||||
contentLength: number
|
||||
|
||||
/** Get response text */
|
||||
text(): string
|
||||
|
||||
@@ -1823,3 +1829,653 @@ declare namespace $app {
|
||||
*/
|
||||
function invalidateClientQuery(queryKeys: string[]): void
|
||||
}
|
||||
|
||||
declare namespace $ui {
|
||||
// Video Core Types
|
||||
|
||||
type PlayerType = "native" | "web"
|
||||
type PlaybackType = "localfile" | "torrent" | "debrid" | "nakama" | "onlinestream"
|
||||
|
||||
type VideoEventType =
|
||||
| "video-loaded"
|
||||
| "video-loaded-metadata"
|
||||
| "video-can-play"
|
||||
| "video-paused"
|
||||
| "video-resumed"
|
||||
| "video-status"
|
||||
| "video-completed"
|
||||
| "video-fullscreen"
|
||||
| "video-pip"
|
||||
| "video-subtitle-track"
|
||||
| "video-media-caption-track"
|
||||
| "video-anime-4k"
|
||||
| "video-audio-track"
|
||||
| "video-ended"
|
||||
| "video-seeked"
|
||||
| "video-error"
|
||||
| "video-terminated"
|
||||
| "video-playback-state"
|
||||
| "subtitle-file-uploaded"
|
||||
| "video-playlist"
|
||||
|
||||
interface BaseVideoEvent {
|
||||
playerType: PlayerType
|
||||
playbackType: PlaybackType
|
||||
playbackId: string
|
||||
clientId: string
|
||||
}
|
||||
|
||||
interface VideoLoadedEvent extends BaseVideoEvent {
|
||||
clientId: string
|
||||
state: PlaybackState
|
||||
}
|
||||
|
||||
interface VideoPlaybackStateEvent extends BaseVideoEvent {
|
||||
clientId: string
|
||||
state: PlaybackState
|
||||
}
|
||||
|
||||
interface VideoPausedEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
interface VideoResumedEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
interface VideoEndedEvent extends BaseVideoEvent {
|
||||
autoNext: boolean
|
||||
}
|
||||
|
||||
interface VideoErrorEvent extends BaseVideoEvent {
|
||||
error: string
|
||||
}
|
||||
|
||||
interface VideoSeekedEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
interface VideoStatusEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
interface VideoLoadedMetadataEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
interface VideoCanPlayEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
interface SubtitleFileUploadedEvent extends BaseVideoEvent {
|
||||
filename: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface VideoTerminatedEvent extends BaseVideoEvent {
|
||||
}
|
||||
|
||||
interface VideoCompletedEvent extends BaseVideoEvent {
|
||||
currentTime: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
interface VideoAudioTrackEvent extends BaseVideoEvent {
|
||||
trackNumber: number
|
||||
isHLS: boolean
|
||||
}
|
||||
|
||||
interface VideoSubtitleTrackEvent extends BaseVideoEvent {
|
||||
trackNumber: number
|
||||
kind: "file" | "event"
|
||||
}
|
||||
|
||||
interface VideoMediaCaptionTrackEvent extends BaseVideoEvent {
|
||||
trackIndex: number
|
||||
}
|
||||
|
||||
interface VideoFullscreenEvent extends BaseVideoEvent {
|
||||
fullscreen: boolean
|
||||
}
|
||||
|
||||
interface VideoPipEvent extends BaseVideoEvent {
|
||||
pip: boolean
|
||||
}
|
||||
|
||||
interface VideoAnime4KEvent extends BaseVideoEvent {
|
||||
option: string
|
||||
}
|
||||
|
||||
interface VideoPlaylistEvent extends BaseVideoEvent {
|
||||
playlist: VideoPlaylistState | null
|
||||
}
|
||||
|
||||
interface VideoTextTracksvent extends BaseVideoEvent {
|
||||
textTracks: VideoTextTrack[]
|
||||
}
|
||||
|
||||
type VideoEvent =
|
||||
| VideoLoadedEvent
|
||||
| VideoPlaybackStateEvent
|
||||
| VideoPausedEvent
|
||||
| VideoResumedEvent
|
||||
| VideoEndedEvent
|
||||
| VideoErrorEvent
|
||||
| VideoSeekedEvent
|
||||
| VideoStatusEvent
|
||||
| VideoLoadedMetadataEvent
|
||||
| VideoCanPlayEvent
|
||||
| SubtitleFileUploadedEvent
|
||||
| VideoTerminatedEvent
|
||||
| VideoCompletedEvent
|
||||
| VideoAudioTrackEvent
|
||||
| VideoSubtitleTrackEvent
|
||||
| VideoMediaCaptionTrackEvent
|
||||
| VideoFullscreenEvent
|
||||
| VideoPipEvent
|
||||
| VideoAnime4KEvent
|
||||
| VideoPlaylistEvent
|
||||
|
||||
interface VideoSubtitleTrack {
|
||||
index: number
|
||||
src?: string
|
||||
content?: string
|
||||
label: string
|
||||
language: string
|
||||
type?: "srt" | "vtt" | "ass" | "ssa"
|
||||
default?: boolean
|
||||
useLibassRenderer?: boolean
|
||||
}
|
||||
|
||||
interface VideoTextTrack {
|
||||
number: number,
|
||||
type: "subtitles" | "captions",
|
||||
label: string,
|
||||
language: string,
|
||||
}
|
||||
|
||||
interface VideoSource {
|
||||
index: number
|
||||
resolution: string
|
||||
url?: string
|
||||
label?: string
|
||||
moreInfo?: string
|
||||
}
|
||||
|
||||
interface VideoInitialState {
|
||||
currentTime?: number
|
||||
paused?: boolean
|
||||
}
|
||||
|
||||
interface OnlinestreamParams {
|
||||
mediaId: number
|
||||
episodeNumber: number
|
||||
provider: string
|
||||
server: string
|
||||
quality: string
|
||||
dubbed: boolean
|
||||
}
|
||||
|
||||
export type MkvTrackType = "video" | "audio" | "subtitle" | "logo" | "buttons" | "complex" | "unknown"
|
||||
|
||||
export type MkvAttachmentType = "font" | "subtitle" | "other"
|
||||
|
||||
export interface MkvTrackInfo {
|
||||
number: number
|
||||
uid: number
|
||||
type: MkvTrackType
|
||||
codecID: string
|
||||
name?: string
|
||||
language?: string
|
||||
languageIETF?: string
|
||||
default: boolean
|
||||
forced: boolean
|
||||
enabled: boolean
|
||||
codecPrivate?: string
|
||||
video?: any
|
||||
audio?: any
|
||||
contentEncodings?: any
|
||||
defaultDuration?: number
|
||||
}
|
||||
|
||||
export interface MkvChapterInfo {
|
||||
uid: number
|
||||
start: number
|
||||
end?: number
|
||||
text?: string
|
||||
languages?: string[]
|
||||
languagesIETF?: string[]
|
||||
editionUID?: number
|
||||
}
|
||||
|
||||
export interface MkvAttachmentInfo {
|
||||
uid: number
|
||||
filename: string
|
||||
mimetype: string
|
||||
size: number
|
||||
description?: string
|
||||
type?: MkvAttachmentType
|
||||
data?: Uint8Array
|
||||
isCompressed?: boolean
|
||||
}
|
||||
|
||||
export interface MkvMetadata {
|
||||
title?: string
|
||||
duration: number
|
||||
timecodeScale: number
|
||||
muxingApp?: string
|
||||
writingApp?: string
|
||||
tracks: MkvTrackInfo[]
|
||||
videoTracks: MkvTrackInfo[]
|
||||
audioTracks: MkvTrackInfo[]
|
||||
subtitleTracks: MkvTrackInfo[]
|
||||
chapters: MkvChapterInfo[]
|
||||
attachments: MkvAttachmentInfo[]
|
||||
mimeCodec?: string
|
||||
error?: Error
|
||||
}
|
||||
|
||||
interface VideoPlaybackInfo {
|
||||
id: string
|
||||
playbackType: PlaybackType
|
||||
streamUrl: string
|
||||
mkvMetadata?: MkvMetadata
|
||||
localFile?: $app.Anime_LocalFile
|
||||
onlinestreamParams?: OnlinestreamParams
|
||||
subtitleTracks: VideoSubtitleTrack[]
|
||||
videoSources: VideoSource[]
|
||||
selectedVideoSource?: number
|
||||
playlistExternalEpisodeNumbers: number[]
|
||||
disableRestoreFromContinuity?: boolean
|
||||
initialState?: VideoInitialState
|
||||
media?: $app.AL_BaseAnime
|
||||
episode?: $app.Anime_Episode
|
||||
streamType: "native" | "hls" | "unknown"
|
||||
isNakamaWatchParty?: boolean
|
||||
}
|
||||
|
||||
interface VideoPlaylistState {
|
||||
type: PlaybackType
|
||||
episodes: $app.Anime_Episode[]
|
||||
previousEpisode?: $app.Anime_Episode
|
||||
nextEpisode?: $app.Anime_Episode
|
||||
currentEpisode: $app.Anime_Episode
|
||||
animeEntry?: $app.Anime_Entry
|
||||
}
|
||||
|
||||
interface PlaybackStatus {
|
||||
id: string
|
||||
clientId: string
|
||||
paused: boolean
|
||||
currentTime: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
interface PlaybackState {
|
||||
clientId: string
|
||||
playerType: PlayerType
|
||||
playbackInfo: VideoPlaybackInfo
|
||||
}
|
||||
|
||||
interface VideoCore {
|
||||
/**
|
||||
* Adds an event listener for video-loaded events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-loaded", callback: (event: VideoLoadedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-playback-state events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-playback-state", callback: (event: VideoPlaybackStateEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-paused events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-paused", callback: (event: VideoPausedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-resumed events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-resumed", callback: (event: VideoResumedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-ended events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-ended", callback: (event: VideoEndedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-error events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-error", callback: (event: VideoErrorEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-seeked events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-seeked", callback: (event: VideoSeekedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-status events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-status", callback: (event: VideoStatusEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-loaded-metadata events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-loaded-metadata", callback: (event: VideoLoadedMetadataEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-can-play events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-can-play", callback: (event: VideoCanPlayEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for subtitle-file-uploaded events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "subtitle-file-uploaded", callback: (event: SubtitleFileUploadedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-terminated events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-terminated", callback: (event: VideoTerminatedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-completed events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-completed", callback: (event: VideoCompletedEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-audio-track events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-audio-track", callback: (event: VideoAudioTrackEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-subtitle-track events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-subtitle-track", callback: (event: VideoSubtitleTrackEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-media-caption-track events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-media-caption-track", callback: (event: VideoMediaCaptionTrackEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-fullscreen events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-fullscreen", callback: (event: VideoFullscreenEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-pip events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-pip", callback: (event: VideoPipEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-anime-4k events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-anime-4k", callback: (event: VideoAnime4KEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for video-playlist events
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: "video-playlist", callback: (event: VideoPlaylistEvent) => void): void
|
||||
|
||||
/**
|
||||
* Adds an event listener for any video event (fallback)
|
||||
* @param eventType - The event type to listen for
|
||||
* @param callback - The callback function to execute when the event is triggered
|
||||
*/
|
||||
addEventListener(eventType: VideoEventType, callback: (event: VideoEvent) => void): void
|
||||
|
||||
/**
|
||||
* Removes an event listener for the specified event type
|
||||
* @param eventType - The event type to stop listening for
|
||||
*/
|
||||
removeEventListener(eventType: VideoEventType): void
|
||||
|
||||
// Playback control methods
|
||||
|
||||
/**
|
||||
* Pauses the video playback
|
||||
*/
|
||||
pause(): void
|
||||
|
||||
/**
|
||||
* Resumes the video playback
|
||||
*/
|
||||
resume(): void
|
||||
|
||||
/**
|
||||
* Seeks forward or backward by the specified number of seconds
|
||||
* @param seconds - Number of seconds to seek (positive for forward, negative for backward)
|
||||
*/
|
||||
seek(seconds: number): void
|
||||
|
||||
/**
|
||||
* Seeks to an absolute position in the video
|
||||
* @param seconds - The absolute position in seconds
|
||||
*/
|
||||
seekTo(seconds: number): void
|
||||
|
||||
/**
|
||||
* Terminates the current video playback
|
||||
*/
|
||||
terminate(): void
|
||||
|
||||
/**
|
||||
* Plays the specified episode
|
||||
* @param which - "next", "previous", or an episode ID
|
||||
*/
|
||||
playEpisode(which: string): void
|
||||
|
||||
// UI control methods
|
||||
|
||||
/**
|
||||
* Sets the fullscreen state of the video player
|
||||
* @param fullscreen - Whether to enable fullscreen
|
||||
*/
|
||||
setFullscreen(fullscreen: boolean): void
|
||||
|
||||
/**
|
||||
* Sets the picture-in-picture state of the video player
|
||||
* @param pip - Whether to enable picture-in-picture
|
||||
*/
|
||||
setPip(pip: boolean): void
|
||||
|
||||
/**
|
||||
* Shows a message in the video player
|
||||
* @param message - The message to display
|
||||
*/
|
||||
showMessage(message: string): void
|
||||
|
||||
// Track control methods
|
||||
|
||||
/**
|
||||
* Sets the active subtitle track
|
||||
* @param trackNumber - The track number to activate
|
||||
*/
|
||||
setSubtitleTrack(trackNumber: number): void
|
||||
|
||||
/**
|
||||
* Adds a subtitle track to the video player.
|
||||
* @important Use addExternalSubtitleTrack instead.
|
||||
* @param track - The subtitle track information
|
||||
*/
|
||||
addSubtitleTrack(track: any): void
|
||||
|
||||
/**
|
||||
* Adds an external subtitle track to the video player
|
||||
* @param track - The external subtitle track information
|
||||
*/
|
||||
addExternalSubtitleTrack(track: Omit<VideoSubtitleTrack, "index" | "useLibassRenderer">): void
|
||||
|
||||
/**
|
||||
* Sets the active media caption track
|
||||
* @param trackIndex - The track index to activate
|
||||
*/
|
||||
setMediaCaptionTrack(trackIndex: number): void
|
||||
|
||||
/**
|
||||
* Adds a media caption track to the video player
|
||||
* @important Use addExternalSubtitleTrack instead.
|
||||
* @param track - The media caption track information
|
||||
*/
|
||||
addMediaCaptionTrack(track: any): void
|
||||
|
||||
/**
|
||||
* Sets the active audio track
|
||||
* @param trackNumber - The track number to activate
|
||||
*/
|
||||
setAudioTrack(trackNumber: number): void
|
||||
|
||||
// State request methods
|
||||
|
||||
/**
|
||||
* Requests the current fullscreen state from the player
|
||||
*/
|
||||
sendGetFullscreen(): void
|
||||
|
||||
/**
|
||||
* Requests the current picture-in-picture state from the player
|
||||
*/
|
||||
sendGetPip(): void
|
||||
|
||||
/**
|
||||
* Requests the current Anime4K state from the player
|
||||
*/
|
||||
sendGetAnime4K(): void
|
||||
|
||||
/**
|
||||
* Requests the current subtitle track from the player
|
||||
*/
|
||||
sendGetSubtitleTrack(): void
|
||||
|
||||
/**
|
||||
* Requests the current audio track from the player
|
||||
*/
|
||||
sendGetAudioTrack(): void
|
||||
|
||||
/**
|
||||
* Requests the current media caption track from the player
|
||||
*/
|
||||
sendGetMediaCaptionTrack(): void
|
||||
|
||||
/**
|
||||
* Requests the current playback state from the player
|
||||
*/
|
||||
sendGetPlaybackState(): void
|
||||
|
||||
// Async getters
|
||||
|
||||
/**
|
||||
* Gets the current text tracks
|
||||
* @returns A promise that resolves to the text tracks or undefined
|
||||
*/
|
||||
getTextTracks(): Promise<VideoTextTrack[] | undefined>
|
||||
|
||||
/**
|
||||
* Gets the current playlist state
|
||||
* @returns A promise that resolves to the playlist state or undefined
|
||||
*/
|
||||
getPlaylist(): Promise<VideoPlaylistState | undefined>
|
||||
|
||||
/**
|
||||
* Pulls the current playback status from the player
|
||||
* @returns A promise that resolves to the video status event
|
||||
*/
|
||||
pullStatus(): Promise<VideoStatusEvent | undefined>
|
||||
|
||||
// Sync getters
|
||||
|
||||
/**
|
||||
* Gets the current playback status
|
||||
* @returns The playback status or undefined
|
||||
*/
|
||||
getPlaybackStatus(): PlaybackStatus | undefined
|
||||
|
||||
/**
|
||||
* Gets the current playback state
|
||||
* @returns The playback state or undefined
|
||||
*/
|
||||
getPlaybackState(): PlaybackState | undefined
|
||||
|
||||
/**
|
||||
* Gets the current playback information
|
||||
* @returns The playback information or undefined
|
||||
*/
|
||||
getCurrentPlaybackInfo(): VideoPlaybackInfo | undefined
|
||||
|
||||
/**
|
||||
* Gets the current media being played
|
||||
* @returns The media information or undefined
|
||||
*/
|
||||
getCurrentMedia(): $app.AL_BaseAnime | undefined
|
||||
|
||||
/**
|
||||
* Gets the current client ID
|
||||
* @returns The client ID or empty string
|
||||
*/
|
||||
getCurrentClientId(): string
|
||||
|
||||
/**
|
||||
* Gets the current player type
|
||||
* @returns The player type or empty string
|
||||
*/
|
||||
getCurrentPlayerType(): PlayerType | ""
|
||||
|
||||
/**
|
||||
* Gets the current playback type
|
||||
* @returns The playback type or empty string
|
||||
*/
|
||||
getCurrentPlaybackType(): PlaybackType | ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package goja_bindings
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -86,7 +86,7 @@ func BindChromeDP(vm *goja.Runtime) *ChromeDP {
|
||||
}
|
||||
|
||||
// BindChromeDPWithScheduler binds the ChromeDP utilities to the VM
|
||||
func BindChromeDPWithScheduler(vm *goja.Runtime, scheduler *goja_util.Scheduler) *ChromeDP {
|
||||
func BindChromeDPWithScheduler(vm *goja.Runtime, scheduler *gojautil.Scheduler) *ChromeDP {
|
||||
c := NewChromeDP(vm)
|
||||
|
||||
// Create ChromeDP object with methods
|
||||
|
||||
@@ -297,6 +297,8 @@ func (wpm *WatchPartyManager) LeaveWatchParty() error {
|
||||
PeerId: hostConn.PeerId,
|
||||
})
|
||||
|
||||
wpm.currentSession = mo.None[*WatchPartySession]()
|
||||
|
||||
// Send websocket event to update the UI (nil indicates session left)
|
||||
wpm.manager.wsEventManager.SendEvent(events.NakamaWatchPartyState, nil)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"seanime/internal/goja/goja_bindings"
|
||||
"seanime/internal/hook"
|
||||
"seanime/internal/library/anime"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -18,10 +18,10 @@ type Anime struct {
|
||||
vm *goja.Runtime
|
||||
logger *zerolog.Logger
|
||||
ext *extension.Extension
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindAnimeToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindAnimeToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
m := &Anime{
|
||||
ctx: a,
|
||||
vm: vm,
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"seanime/internal/torrentstream"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/filecache"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"seanime/internal/videocore"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
@@ -73,62 +73,65 @@ type AppContext interface {
|
||||
|
||||
BindApp(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension)
|
||||
// BindStorage binds $storage to the Goja runtime
|
||||
BindStorage(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Storage
|
||||
BindStorage(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) *Storage
|
||||
// BindAnilist binds $anilist to the Goja runtime
|
||||
BindAnilist(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension)
|
||||
// BindDatabase binds $database to the Goja runtime
|
||||
BindDatabase(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension)
|
||||
|
||||
// BindSystem binds $system to the Goja runtime
|
||||
BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindPlaybackToContextObj binds 'playback' to the UI context object
|
||||
BindPlaybackToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindPlaybackToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindVideoCoreToContextObj binds 'videoCore' to the UI context object
|
||||
BindVideoCoreToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindCronToContextObj binds 'cron' to the UI context object
|
||||
BindCronToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Cron
|
||||
BindCronToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) *Cron
|
||||
|
||||
// BindDownloaderToContextObj binds 'downloader' to the UI context object
|
||||
BindDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindMangaToContextObj binds 'manga' to the UI context object
|
||||
BindMangaToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindMangaToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindAnimeToContextObj binds 'anime' to the UI context object
|
||||
BindAnimeToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindAnimeToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindDiscordToContextObj binds 'discord' to the UI context object
|
||||
BindDiscordToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindDiscordToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindContinuityToContextObj binds 'continuity' to the UI context object
|
||||
BindContinuityToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindContinuityToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindTorrentClientToContextObj binds 'torrentClient' to the UI context object
|
||||
BindTorrentClientToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindTorrentClientToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindTorrentstreamToContextObj binds 'torrentstream' to the UI context object
|
||||
BindTorrentstreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindTorrentstreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindMediastreamToContextObj binds 'mediastream' to the UI context object
|
||||
BindMediastreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindMediastreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindOnlinestreamToContextObj binds 'onlinestream' to the UI context object
|
||||
BindOnlinestreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindOnlinestreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindFillerManagerToContextObj binds 'fillerManager' to the UI context object
|
||||
BindFillerManagerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindFillerManagerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindAutoDownloaderToContextObj binds 'autoDownloader' to the UI context object
|
||||
BindAutoDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindAutoDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindAutoScannerToContextObj binds 'autoScanner' to the UI context object
|
||||
BindAutoScannerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindAutoScannerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindFileCacherToContextObj binds 'fileCacher' to the UI context object
|
||||
BindFileCacherToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindFileCacherToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
// BindExternalPlayerLinkToContextObj binds 'externalPlayerLink' to the UI context object
|
||||
BindExternalPlayerLinkToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
BindExternalPlayerLinkToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler)
|
||||
|
||||
DropPluginData(extId string)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"seanime/internal/continuity"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/goja/goja_bindings"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (a *AppContextImpl) BindContinuityToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindContinuityToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
continuityObj := vm.NewObject()
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/extension"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -33,7 +33,7 @@ type Cron struct {
|
||||
jobs []*CronJob
|
||||
interval time.Duration
|
||||
mux sync.RWMutex
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
}
|
||||
|
||||
// New create a new Cron struct with default tick interval of 1 minute
|
||||
@@ -41,7 +41,7 @@ type Cron struct {
|
||||
//
|
||||
// You can change the default tick interval with Cron.SetInterval().
|
||||
// You can change the default timezone with Cron.SetTimezone().
|
||||
func New(scheduler *goja_util.Scheduler) *Cron {
|
||||
func New(scheduler *gojautil.Scheduler) *Cron {
|
||||
return &Cron{
|
||||
interval: 1 * time.Minute,
|
||||
timezone: time.UTC,
|
||||
@@ -51,7 +51,7 @@ func New(scheduler *goja_util.Scheduler) *Cron {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindCronToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Cron {
|
||||
func (a *AppContextImpl) BindCronToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) *Cron {
|
||||
cron := New(scheduler)
|
||||
cronObj := vm.NewObject()
|
||||
_ = cronObj.Set("add", cron.Add)
|
||||
@@ -262,7 +262,7 @@ type CronJob struct {
|
||||
fn func()
|
||||
schedule *Schedule
|
||||
id string
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
}
|
||||
|
||||
// Id returns the cron job id.
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
discordrpc_presence "seanime/internal/discordrpc/presence"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/goja/goja_bindings"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (a *AppContextImpl) BindDiscordToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindDiscordToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
discordObj := vm.NewObject()
|
||||
_ = discordObj.Set("setMangaActivity", func(opts discordrpc_presence.MangaActivity) goja.Value {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"seanime/internal/extension"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -55,7 +55,7 @@ type progressSubscriber struct {
|
||||
LastSent time.Time
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
downloadObj := vm.NewObject()
|
||||
|
||||
progressMap := sync.Map{}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/goja/goja_bindings"
|
||||
"seanime/internal/manga"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -17,10 +17,10 @@ type Manga struct {
|
||||
vm *goja.Runtime
|
||||
logger *zerolog.Logger
|
||||
ext *extension.Extension
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindMangaToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindMangaToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
m := &Manga{
|
||||
ctx: a,
|
||||
vm: vm,
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"seanime/internal/goja/goja_bindings"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/onlinestream"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"strconv"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
@@ -14,22 +14,22 @@ import (
|
||||
)
|
||||
|
||||
// BindTorrentstreamToContextObj binds 'torrentstream' to the UI context object
|
||||
func (a *AppContextImpl) BindTorrentstreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindTorrentstreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
}
|
||||
|
||||
// BindOnlinestreamToContextObj binds 'onlinestream' to the UI context object
|
||||
func (a *AppContextImpl) BindOnlinestreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindOnlinestreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
}
|
||||
|
||||
// BindMediastreamToContextObj binds 'mediastream' to the UI context object
|
||||
func (a *AppContextImpl) BindMediastreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindMediastreamToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
}
|
||||
|
||||
// BindTorrentClientToContextObj binds 'torrentClient' to the UI context object
|
||||
func (a *AppContextImpl) BindTorrentClientToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindTorrentClientToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
torrentClientObj := vm.NewObject()
|
||||
_ = torrentClientObj.Set("getTorrents", func() goja.Value {
|
||||
@@ -221,7 +221,7 @@ func (a *AppContextImpl) BindTorrentClientToContextObj(vm *goja.Runtime, obj *go
|
||||
}
|
||||
|
||||
// BindFillerManagerToContextObj binds 'fillerManager' to the UI context object
|
||||
func (a *AppContextImpl) BindFillerManagerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindFillerManagerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
fillerManagerObj := vm.NewObject()
|
||||
_ = fillerManagerObj.Set("getFillerEpisodes", func(mediaId int) goja.Value {
|
||||
@@ -285,7 +285,7 @@ func (a *AppContextImpl) BindFillerManagerToContextObj(vm *goja.Runtime, obj *go
|
||||
}
|
||||
|
||||
// BindAutoDownloaderToContextObj binds 'autoDownloader' to the UI context object
|
||||
func (a *AppContextImpl) BindAutoDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindAutoDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
autoDownloaderObj := vm.NewObject()
|
||||
_ = autoDownloaderObj.Set("run", func() goja.Value {
|
||||
@@ -300,7 +300,7 @@ func (a *AppContextImpl) BindAutoDownloaderToContextObj(vm *goja.Runtime, obj *g
|
||||
}
|
||||
|
||||
// BindAutoScannerToContextObj binds 'autoScanner' to the UI context object
|
||||
func (a *AppContextImpl) BindAutoScannerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindAutoScannerToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
autoScannerObj := vm.NewObject()
|
||||
_ = autoScannerObj.Set("notify", func() goja.Value {
|
||||
@@ -316,12 +316,12 @@ func (a *AppContextImpl) BindAutoScannerToContextObj(vm *goja.Runtime, obj *goja
|
||||
}
|
||||
|
||||
// BindFileCacherToContextObj binds 'fileCacher' to the UI context object
|
||||
func (a *AppContextImpl) BindFileCacherToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindFileCacherToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
}
|
||||
|
||||
// BindExternalPlayerLinkToContextObj binds 'externalPlayerLink' to the UI context object
|
||||
func (a *AppContextImpl) BindExternalPlayerLinkToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindExternalPlayerLinkToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
externalPlayerLinkObj := vm.NewObject()
|
||||
_ = externalPlayerLinkObj.Set("open", func(url string, mediaId int, episodeNumber int, mediaTitle string) goja.Value {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"seanime/internal/mediaplayers/mediaplayer"
|
||||
"seanime/internal/mediaplayers/mpv"
|
||||
"seanime/internal/mediaplayers/mpvipc"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/google/uuid"
|
||||
@@ -20,7 +20,7 @@ type Playback struct {
|
||||
vm *goja.Runtime
|
||||
logger *zerolog.Logger
|
||||
ext *extension.Extension
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
}
|
||||
|
||||
type PlaybackMPV struct {
|
||||
@@ -28,7 +28,7 @@ type PlaybackMPV struct {
|
||||
playback *Playback
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindPlaybackToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindPlaybackToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
p := &Playback{
|
||||
ctx: a,
|
||||
vm: vm,
|
||||
@@ -43,7 +43,7 @@ func (a *AppContextImpl) BindPlaybackToContextObj(vm *goja.Runtime, obj *goja.Ob
|
||||
_ = playbackObj.Set("registerEventListener", p.registerEventListener)
|
||||
_ = playbackObj.Set("pause", p.pause)
|
||||
_ = playbackObj.Set("resume", p.resume)
|
||||
_ = playbackObj.Set("seek", p.seek)
|
||||
_ = playbackObj.Set("seekTo", p.seekTo)
|
||||
_ = playbackObj.Set("cancel", p.cancel)
|
||||
_ = playbackObj.Set("getNextEpisode", p.getNextEpisode)
|
||||
_ = playbackObj.Set("playNextEpisode", p.playNextEpisode)
|
||||
@@ -307,7 +307,7 @@ func (p *Playback) resume() error {
|
||||
return playbackManager.Resume()
|
||||
}
|
||||
|
||||
func (p *Playback) seek(seconds float64) error {
|
||||
func (p *Playback) seekTo(seconds float64) error {
|
||||
playbackManager, ok := p.ctx.PlaybackManager().Get()
|
||||
if !ok {
|
||||
return errors.New("playback manager not found")
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"seanime/internal/database/models"
|
||||
"seanime/internal/extension"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"seanime/internal/util/result"
|
||||
"strings"
|
||||
|
||||
@@ -24,7 +24,7 @@ type Storage struct {
|
||||
pluginDataCache *result.Map[string, *models.PluginData] // Cache to avoid repeated database calls
|
||||
keyDataCache *result.Map[string, interface{}] // Cache to avoid repeated database calls
|
||||
keySubscribers *result.Map[string, []chan interface{}] // Subscribers for key changes
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -34,7 +34,7 @@ var (
|
||||
// BindStorage binds the storage API to the Goja runtime.
|
||||
// Permissions need to be checked by the caller.
|
||||
// Permissions needed: storage
|
||||
func (a *AppContextImpl) BindStorage(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Storage {
|
||||
func (a *AppContextImpl) BindStorage(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) *Storage {
|
||||
storageLogger := logger.With().Str("id", ext.ID).Logger()
|
||||
storage := &Storage{
|
||||
ctx: a,
|
||||
|
||||
@@ -4,7 +4,7 @@ package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"seanime/internal/util/result"
|
||||
"sync"
|
||||
|
||||
@@ -55,7 +55,7 @@ func (s *Store[K, T]) Stop() {
|
||||
s.keySubscribers.Clear()
|
||||
}
|
||||
|
||||
func (s *Store[K, T]) Bind(vm *goja.Runtime, scheduler *goja_util.Scheduler) {
|
||||
func (s *Store[K, T]) Bind(vm *goja.Runtime, scheduler *gojautil.Scheduler) {
|
||||
// Create a new object for the store
|
||||
storeObj := vm.NewObject()
|
||||
_ = storeObj.Set("get", s.Get)
|
||||
@@ -76,7 +76,7 @@ func (s *Store[K, T]) Bind(vm *goja.Runtime, scheduler *goja_util.Scheduler) {
|
||||
}
|
||||
|
||||
// BindWatch binds the watch method to the store object in the runtime.
|
||||
func (s *Store[K, T]) bindWatch(storeObj *goja.Object, vm *goja.Runtime, scheduler *goja_util.Scheduler) {
|
||||
func (s *Store[K, T]) bindWatch(storeObj *goja.Object, vm *goja.Runtime, scheduler *gojautil.Scheduler) {
|
||||
|
||||
// Example:
|
||||
// store.watch("key", (value) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"runtime"
|
||||
"seanime/internal/extension"
|
||||
util "seanime/internal/util"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -38,7 +38,7 @@ type AsyncCmd struct {
|
||||
cmd *exec.Cmd
|
||||
|
||||
appContext *AppContextImpl
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
vm *goja.Runtime
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ type CmdHelper struct {
|
||||
stderr io.ReadCloser
|
||||
|
||||
appContext *AppContextImpl
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
vm *goja.Runtime
|
||||
}
|
||||
|
||||
// BindSystem binds the system module to the Goja runtime.
|
||||
// Permissions needed: system + allowlist
|
||||
func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
|
||||
//////////////////////////////////////
|
||||
// OS
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package plugin_ui
|
||||
|
||||
import (
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"seanime/internal/util/result"
|
||||
"slices"
|
||||
"sync"
|
||||
@@ -334,7 +334,7 @@ func (c *CommandPaletteManager) jsNewCommandPalette(options NewCommandPaletteOpt
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (c *commandItem) ToJSON(ctx *Context, componentManager *ComponentManager, scheduler *goja_util.Scheduler) *CommandItemJSON {
|
||||
func (c *commandItem) ToJSON(ctx *Context, componentManager *ComponentManager, scheduler *gojautil.Scheduler) *CommandItemJSON {
|
||||
|
||||
var components interface{}
|
||||
if c.renderFunc != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/plugin"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"seanime/internal/util/result"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -49,7 +49,7 @@ type Context struct {
|
||||
vm *goja.Runtime
|
||||
states *result.Map[string, *State]
|
||||
stateSubscribers []chan *State
|
||||
scheduler *goja_util.Scheduler // Schedule VM executions concurrently and execute them in order.
|
||||
scheduler *gojautil.Scheduler // Schedule VM executions concurrently and execute them in order.
|
||||
wsSubscriber *events.ClientEventSubscriber
|
||||
eventBus *result.Map[ClientEventType, *result.Map[string, *EventListener]] // map[string]map[string]*EventListener (event -> listenerID -> listener)
|
||||
contextObj *goja.Object
|
||||
@@ -203,6 +203,7 @@ func (c *Context) createAndBindContextObject(vm *goja.Runtime) {
|
||||
case extension.PluginPermissionPlayback:
|
||||
// Bind playback to the context object
|
||||
plugin.GlobalAppContext.BindPlaybackToContextObj(vm, obj, c.logger, c.ext, c.scheduler)
|
||||
plugin.GlobalAppContext.BindVideoCoreToContextObj(vm, obj, c.logger, c.ext, c.scheduler)
|
||||
case extension.PluginPermissionSystem:
|
||||
plugin.GlobalAppContext.BindDownloaderToContextObj(vm, obj, c.logger, c.ext, c.scheduler)
|
||||
case extension.PluginPermissionCron:
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/plugin"
|
||||
"seanime/internal/util"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"sync"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
@@ -38,7 +38,7 @@ type UI struct {
|
||||
logger *zerolog.Logger
|
||||
wsEventManager events.WSEventManagerInterface
|
||||
appContext plugin.AppContext
|
||||
scheduler *goja_util.Scheduler
|
||||
scheduler *gojautil.Scheduler
|
||||
|
||||
lastException string
|
||||
|
||||
@@ -53,7 +53,7 @@ type NewUIOptions struct {
|
||||
VM *goja.Runtime
|
||||
WSManager events.WSEventManagerInterface
|
||||
Database *db.Database
|
||||
Scheduler *goja_util.Scheduler
|
||||
Scheduler *gojautil.Scheduler
|
||||
Extension *extension.Extension
|
||||
}
|
||||
|
||||
|
||||
656
internal/plugin/videocore.go
Normal file
656
internal/plugin/videocore.go
Normal file
@@ -0,0 +1,656 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/mkvparser"
|
||||
gojautil "seanime/internal/util/goja"
|
||||
"seanime/internal/util/result"
|
||||
"seanime/internal/videocore"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type VideoCore struct {
|
||||
ctx *AppContextImpl
|
||||
vm *goja.Runtime
|
||||
logger *zerolog.Logger
|
||||
ext *extension.Extension
|
||||
scheduler *gojautil.Scheduler
|
||||
listeners *result.Map[string, *VideoCoreEventListener]
|
||||
videoCoreSubscriber *videocore.Subscriber
|
||||
unsubscribeOnce sync.Once
|
||||
}
|
||||
|
||||
type VideoCoreEventListener struct {
|
||||
eventId string
|
||||
listenerCh chan videocore.VideoEvent
|
||||
closed atomic.Bool
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (a *AppContextImpl) BindVideoCoreToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *gojautil.Scheduler) {
|
||||
p := &VideoCore{
|
||||
ctx: a,
|
||||
vm: vm,
|
||||
logger: logger,
|
||||
ext: ext,
|
||||
scheduler: scheduler,
|
||||
listeners: result.NewMap[string, *VideoCoreEventListener](),
|
||||
}
|
||||
|
||||
vcObj := vm.NewObject()
|
||||
// Event listeners
|
||||
_ = vcObj.Set("addEventListener", p.addEventListener)
|
||||
_ = vcObj.Set("removeEventListener", p.removeEventListener)
|
||||
|
||||
// Playback control
|
||||
_ = vcObj.Set("pause", p.pause)
|
||||
_ = vcObj.Set("resume", p.resume)
|
||||
_ = vcObj.Set("seek", p.seek)
|
||||
_ = vcObj.Set("seekTo", p.seekTo)
|
||||
_ = vcObj.Set("terminate", p.terminate)
|
||||
_ = vcObj.Set("playEpisode", p.playEpisode)
|
||||
|
||||
// UI control
|
||||
_ = vcObj.Set("setFullscreen", p.setFullscreen)
|
||||
_ = vcObj.Set("setPip", p.setPip)
|
||||
_ = vcObj.Set("showMessage", p.showMessage)
|
||||
|
||||
// Track control
|
||||
_ = vcObj.Set("setSubtitleTrack", p.setSubtitleTrack)
|
||||
_ = vcObj.Set("addSubtitleTrack", p.addSubtitleTrack)
|
||||
_ = vcObj.Set("addExternalSubtitleTrack", p.addExternalSubtitleTrack)
|
||||
_ = vcObj.Set("setMediaCaptionTrack", p.setMediaCaptionTrack)
|
||||
_ = vcObj.Set("addMediaCaptionTrack", p.addMediaCaptionTrack)
|
||||
_ = vcObj.Set("setAudioTrack", p.setAudioTrack)
|
||||
|
||||
// State requests
|
||||
_ = vcObj.Set("sendGetFullscreen", p.sendGetFullscreen)
|
||||
_ = vcObj.Set("sendGetPip", p.sendGetPip)
|
||||
_ = vcObj.Set("sendGetAnime4K", p.sendGetAnime4K)
|
||||
_ = vcObj.Set("sendGetSubtitleTrack", p.sendGetSubtitleTrack)
|
||||
_ = vcObj.Set("sendGetAudioTrack", p.sendGetAudioTrack)
|
||||
_ = vcObj.Set("sendGetMediaCaptionTrack", p.sendGetMediaCaptionTrack)
|
||||
_ = vcObj.Set("sendGetPlaybackState", p.sendGetPlaybackState)
|
||||
|
||||
// Async getters
|
||||
_ = vcObj.Set("getPlaylist", p.getPlaylist)
|
||||
_ = vcObj.Set("pullStatus", p.pullStatus)
|
||||
_ = vcObj.Set("getTextTracks", p.getTextTracks)
|
||||
|
||||
// Sync getters
|
||||
_ = vcObj.Set("getPlaybackStatus", p.getPlaybackStatus)
|
||||
_ = vcObj.Set("getPlaybackState", p.getPlaybackState)
|
||||
_ = vcObj.Set("getCurrentPlaybackInfo", p.getCurrentPlaybackInfo)
|
||||
_ = vcObj.Set("getCurrentMedia", p.getCurrentMedia)
|
||||
_ = vcObj.Set("getCurrentClientId", p.getCurrentClientId)
|
||||
_ = vcObj.Set("getCurrentPlayerType", p.getCurrentPlayerType)
|
||||
_ = vcObj.Set("getCurrentPlaybackType", p.getCurrentPlaybackType)
|
||||
|
||||
//_ = vcObj.Set("startOnlinestreamWatchParty", p.startOnlinestreamWatchParty)
|
||||
|
||||
_ = obj.Set("videoCore", vcObj)
|
||||
|
||||
}
|
||||
|
||||
type VideoCoreEvent struct {
|
||||
}
|
||||
|
||||
// getEventType maps a VideoEvent to its event type identifier
|
||||
func (p *VideoCore) getEventType(event videocore.VideoEvent) string {
|
||||
switch event.(type) {
|
||||
case *videocore.VideoLoadedEvent:
|
||||
return string(videocore.PlayerEventVideoLoaded)
|
||||
case *videocore.VideoLoadedMetadataEvent:
|
||||
return string(videocore.PlayerEventVideoLoadedMetadata)
|
||||
case *videocore.VideoCanPlayEvent:
|
||||
return string(videocore.PlayerEventVideoCanPlay)
|
||||
case *videocore.VideoPausedEvent:
|
||||
return string(videocore.PlayerEventVideoPaused)
|
||||
case *videocore.VideoResumedEvent:
|
||||
return string(videocore.PlayerEventVideoResumed)
|
||||
case *videocore.VideoStatusEvent:
|
||||
return string(videocore.PlayerEventVideoStatus)
|
||||
case *videocore.VideoCompletedEvent:
|
||||
return string(videocore.PlayerEventVideoCompleted)
|
||||
case *videocore.VideoFullscreenEvent:
|
||||
return string(videocore.PlayerEventVideoFullscreen)
|
||||
case *videocore.VideoPipEvent:
|
||||
return string(videocore.PlayerEventVideoPip)
|
||||
case *videocore.VideoSubtitleTrackEvent:
|
||||
return string(videocore.PlayerEventVideoSubtitleTrack)
|
||||
case *videocore.VideoMediaCaptionTrackEvent:
|
||||
return string(videocore.PlayerEventMediaCaptionTrack)
|
||||
case *videocore.VideoAnime4KEvent:
|
||||
return string(videocore.PlayerEventAnime4K)
|
||||
case *videocore.VideoAudioTrackEvent:
|
||||
return string(videocore.PlayerEventVideoAudioTrack)
|
||||
case *videocore.VideoEndedEvent:
|
||||
return string(videocore.PlayerEventVideoEnded)
|
||||
case *videocore.VideoSeekedEvent:
|
||||
return string(videocore.PlayerEventVideoSeeked)
|
||||
case *videocore.VideoErrorEvent:
|
||||
return string(videocore.PlayerEventVideoError)
|
||||
case *videocore.VideoTerminatedEvent:
|
||||
return string(videocore.PlayerEventVideoTerminated)
|
||||
case *videocore.VideoPlaybackStateEvent:
|
||||
return string(videocore.PlayerEventVideoPlaybackState)
|
||||
case *videocore.SubtitleFileUploadedEvent:
|
||||
return string(videocore.PlayerEventSubtitleFileUploaded)
|
||||
case *videocore.VideoPlaylistEvent:
|
||||
return string(videocore.PlayerEventVideoPlaylist)
|
||||
case *videocore.VideoTextTracksEvent:
|
||||
return string(videocore.PlayerEventVideoTextTracks)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VideoCore) convertEventToJSObject(event videocore.VideoEvent) goja.Value {
|
||||
return p.vm.ToValue(event)
|
||||
}
|
||||
|
||||
func (p *VideoCore) subscribeToEvents() {
|
||||
p.unsubscribeOnce = sync.Once{}
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.videoCoreSubscriber = videoCore.Subscribe("__plugin_videocore_subscriber__" + p.ext.ID)
|
||||
go func() {
|
||||
for event := range p.videoCoreSubscriber.Events() {
|
||||
p.listeners.Range(func(eventId string, listener *VideoCoreEventListener) bool {
|
||||
if listener.closed.Load() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Filter events based on the event type the listener is subscribed to
|
||||
eventType := p.getEventType(event)
|
||||
if eventType == "" || eventType != listener.eventId {
|
||||
return true
|
||||
}
|
||||
|
||||
select {
|
||||
case listener.listenerCh <- event:
|
||||
default:
|
||||
// Channel is full, drop the event
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// addEventListener registers a subscriber for playback events.
|
||||
//
|
||||
// Example:
|
||||
// ctx.videoCore.addEventListener("video-loaded", (event) => {
|
||||
// console.log(event)
|
||||
// });
|
||||
func (p *VideoCore) addEventListener(call goja.FunctionCall) goja.Value {
|
||||
_, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
panic(p.vm.NewTypeError("videocore not found"))
|
||||
}
|
||||
|
||||
eventId := gojautil.ExpectStringArg(p.vm, call, 0)
|
||||
callback := gojautil.ExpectFunctionArg(p.vm, call, 1)
|
||||
|
||||
listener := &VideoCoreEventListener{
|
||||
eventId: eventId,
|
||||
listenerCh: make(chan videocore.VideoEvent, 100),
|
||||
}
|
||||
|
||||
// If it's the first listener, subscribe to the videocore events
|
||||
listenerCount := len(p.listeners.Keys())
|
||||
if listenerCount == 0 {
|
||||
p.subscribeToEvents()
|
||||
}
|
||||
|
||||
p.listeners.Set(eventId, listener)
|
||||
|
||||
go func() {
|
||||
for e := range listener.listenerCh {
|
||||
if listener.closed.Load() {
|
||||
return
|
||||
}
|
||||
p.scheduler.ScheduleAsync(func() error {
|
||||
eventObj := p.convertEventToJSObject(e)
|
||||
_, err := callback(goja.Undefined(), eventObj)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msgf("plugin: Error calling videoCore event callback for event %s", eventId)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// removeEventListener removes a playback event listener.
|
||||
//
|
||||
// Example:
|
||||
// ctx.videoCore.removeEventListener("video-loaded");
|
||||
func (p *VideoCore) removeEventListener(call goja.FunctionCall) goja.Value {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
panic(p.vm.NewTypeError("videocore not found"))
|
||||
}
|
||||
|
||||
eventId := gojautil.ExpectStringArg(p.vm, call, 0)
|
||||
|
||||
if listener, ok := p.listeners.Pop(eventId); ok {
|
||||
listener.closed.Store(true)
|
||||
listener.closeOnce.Do(func() {
|
||||
close(listener.listenerCh)
|
||||
})
|
||||
}
|
||||
|
||||
// If it's the last listener, unsubscribe from the videocore events
|
||||
listenerCount := len(p.listeners.Keys())
|
||||
if listenerCount == 0 {
|
||||
p.unsubscribeOnce.Do(func() {
|
||||
if p.videoCoreSubscriber != nil {
|
||||
videoCore.Unsubscribe(p.videoCoreSubscriber.GetId())
|
||||
p.videoCoreSubscriber = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
func (p *VideoCore) pause() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.Pause()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) resume() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.Resume()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) seek(seconds float64) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.Seek(seconds)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) seekTo(seconds float64) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SeekTo(seconds)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) terminate() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.Terminate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) getTextTracks() goja.Value {
|
||||
promise, resolve, reject := p.vm.NewPromise()
|
||||
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
reject(p.vm.NewGoError(errors.New("videocore not found")))
|
||||
return p.vm.ToValue(promise)
|
||||
}
|
||||
|
||||
go func() {
|
||||
ret, ok := videoCore.GetTextTracks()
|
||||
p.scheduler.ScheduleAsync(func() error {
|
||||
if ok {
|
||||
resolve(p.vm.ToValue(ret))
|
||||
} else {
|
||||
resolve(goja.Undefined())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
return p.vm.ToValue(promise)
|
||||
}
|
||||
|
||||
func (p *VideoCore) getPlaylist() goja.Value {
|
||||
promise, resolve, reject := p.vm.NewPromise()
|
||||
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
reject(p.vm.NewGoError(errors.New("videocore not found")))
|
||||
return p.vm.ToValue(promise)
|
||||
}
|
||||
|
||||
go func() {
|
||||
playlist, ok := videoCore.GetPlaylist()
|
||||
p.scheduler.ScheduleAsync(func() error {
|
||||
if ok {
|
||||
resolve(p.vm.ToValue(playlist))
|
||||
} else {
|
||||
resolve(goja.Undefined())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
return p.vm.ToValue(promise)
|
||||
}
|
||||
|
||||
func (p *VideoCore) playEpisode(call goja.FunctionCall) goja.Value {
|
||||
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
panic(p.vm.NewTypeError("videocore not found"))
|
||||
}
|
||||
|
||||
which := gojautil.ExpectStringArg(p.vm, call, 0)
|
||||
videoCore.PlayEpisode(which)
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// UI control methods
|
||||
|
||||
func (p *VideoCore) setFullscreen(fullscreen bool) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SetFullscreen(fullscreen)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) setPip(pip bool) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SetPip(pip)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) showMessage(message string) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.ShowMessage(message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Track control methods
|
||||
|
||||
func (p *VideoCore) setSubtitleTrack(trackNumber int) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SetSubtitleTrack(trackNumber)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) addSubtitleTrack(track mkvparser.TrackInfo) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
|
||||
videoCore.AddSubtitleTrack(&track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) addExternalSubtitleTrack(track videocore.VideoSubtitleTrack) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
|
||||
videoCore.AddExternalSubtitleTrack(&track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) setMediaCaptionTrack(trackIndex int) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SetMediaCaptionTrack(trackIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) addMediaCaptionTrack(track interface{}) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
|
||||
videoCore.AddMediaCaptionTrack(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) setAudioTrack(trackNumber int) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SetAudioTrack(trackNumber)
|
||||
return nil
|
||||
}
|
||||
|
||||
// State request methods
|
||||
|
||||
func (p *VideoCore) sendGetFullscreen() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetFullscreen()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) sendGetPip() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetPip()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) sendGetAnime4K() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetAnime4K()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) sendGetSubtitleTrack() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetSubtitleTrack()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) sendGetAudioTrack() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetAudioTrack()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) sendGetMediaCaptionTrack() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetMediaCaptionTrack()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VideoCore) sendGetPlaybackState() error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
videoCore.SendGetPlaybackState()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Async getter methods
|
||||
|
||||
func (p *VideoCore) pullStatus() goja.Value {
|
||||
promise, resolve, reject := p.vm.NewPromise()
|
||||
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
reject(p.vm.NewGoError(errors.New("videocore not found")))
|
||||
return p.vm.ToValue(promise)
|
||||
}
|
||||
|
||||
go func() {
|
||||
status, ok := videoCore.PullStatus()
|
||||
p.scheduler.ScheduleAsync(func() error {
|
||||
if ok {
|
||||
_ = resolve(p.vm.ToValue(status))
|
||||
} else {
|
||||
_ = resolve(goja.Undefined())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
return p.vm.ToValue(promise)
|
||||
}
|
||||
|
||||
// Sync getter methods
|
||||
|
||||
func (p *VideoCore) getPlaybackStatus() goja.Value {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
status, ok := videoCore.GetPlaybackStatus()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
return p.vm.ToValue(status)
|
||||
}
|
||||
|
||||
func (p *VideoCore) getPlaybackState() goja.Value {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
state, ok := videoCore.GetPlaybackState()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
return p.vm.ToValue(state)
|
||||
}
|
||||
|
||||
func (p *VideoCore) getCurrentPlaybackInfo() goja.Value {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
info, ok := videoCore.GetCurrentPlaybackInfo()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
return p.vm.ToValue(info)
|
||||
}
|
||||
|
||||
func (p *VideoCore) getCurrentMedia() goja.Value {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
media, ok := videoCore.GetCurrentMedia()
|
||||
if !ok {
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
return p.vm.ToValue(media)
|
||||
}
|
||||
|
||||
func (p *VideoCore) getCurrentClientId() string {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return videoCore.GetCurrentClientId()
|
||||
}
|
||||
|
||||
func (p *VideoCore) getCurrentPlayerType() string {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
playerType, ok := videoCore.GetCurrentPlayerType()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(playerType)
|
||||
}
|
||||
|
||||
func (p *VideoCore) getCurrentPlaybackType() string {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
playbackType, ok := videoCore.GetCurrentPlaybackType()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(playbackType)
|
||||
}
|
||||
|
||||
// Special methods
|
||||
|
||||
func (p *VideoCore) startOnlinestreamWatchParty(params videocore.OnlinestreamParams) error {
|
||||
videoCore, ok := p.ctx.VideoCore().Get()
|
||||
if !ok {
|
||||
return errors.New("videocore not found")
|
||||
}
|
||||
|
||||
videoCore.StartOnlinestreamWatchParty(¶ms)
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package goja_util
|
||||
package gojautil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
147
internal/util/goja/common.go
Normal file
147
internal/util/goja/common.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package gojautil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// ExpectStringArg ensures the argument exists and is strictly a string.
|
||||
// It panics with a TypeError if validation fails.
|
||||
// Example:
|
||||
//
|
||||
// func do(call goja.FunctionCall) goja.Value {
|
||||
// url := gojautil.ExpectStringArg(vm, call, 0)
|
||||
func ExpectStringArg(vm *goja.Runtime, call goja.FunctionCall, index int) string {
|
||||
arg := call.Argument(index)
|
||||
|
||||
if goja.IsUndefined(arg) {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d is missing", index)))
|
||||
}
|
||||
|
||||
// Export returns the underlying Go value
|
||||
if _, ok := arg.Export().(string); !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d must be a string", index)))
|
||||
}
|
||||
|
||||
return arg.String()
|
||||
}
|
||||
|
||||
// ExpectIntArg ensures the argument exists and is strictly a number (int64 compatible).
|
||||
func ExpectIntArg(vm *goja.Runtime, call goja.FunctionCall, index int) int64 {
|
||||
arg := call.Argument(index)
|
||||
|
||||
if goja.IsUndefined(arg) {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d is missing", index)))
|
||||
}
|
||||
|
||||
// Goja stores numbers as int64 or float64
|
||||
val := arg.ToInteger()
|
||||
// We check if it was actually a number type originally
|
||||
if _, ok := arg.Export().(int64); !ok {
|
||||
if _, ok := arg.Export().(float64); !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d must be a number", index)))
|
||||
}
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// ExpectObjectArg ensures the value is a non-null object and returns the *goja.Object wrapper.
|
||||
func ExpectObjectArg(vm *goja.Runtime, val goja.Value, argName string) *goja.Object {
|
||||
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("%s must be an object", argName)))
|
||||
}
|
||||
|
||||
// Export check ensures it's not just a primitive wrapped as an object
|
||||
// (e.g. strict validation against "new String('a')")
|
||||
if _, ok := val.Export().(map[string]interface{}); !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("%s must be a valid object/map", argName)))
|
||||
}
|
||||
|
||||
return val.ToObject(vm)
|
||||
}
|
||||
|
||||
// ExpectBoolArg ensures the argument is strictly a boolean.
|
||||
func ExpectBoolArg(vm *goja.Runtime, call goja.FunctionCall, index int) bool {
|
||||
arg := call.Argument(index)
|
||||
|
||||
if goja.IsUndefined(arg) {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d is missing", index)))
|
||||
}
|
||||
|
||||
if _, ok := arg.Export().(bool); !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d must be a boolean", index)))
|
||||
}
|
||||
|
||||
return arg.ToBoolean()
|
||||
}
|
||||
|
||||
// ExpectArrayArg ensures the argument is an array and returns the Object wrapper.
|
||||
// You can then iterate over it using .Export().([]interface{}) or key access.
|
||||
func ExpectArrayArg(vm *goja.Runtime, call goja.FunctionCall, index int) *goja.Object {
|
||||
arg := call.Argument(index)
|
||||
|
||||
if goja.IsUndefined(arg) {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d is missing", index)))
|
||||
}
|
||||
|
||||
// Export() of an array returns []interface{}
|
||||
if _, ok := arg.Export().([]interface{}); !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d must be an array", index)))
|
||||
}
|
||||
|
||||
return arg.ToObject(vm)
|
||||
}
|
||||
|
||||
// ExpectFunctionArg ensures the argument is a callable function.
|
||||
// It returns the goja.Callable which you can invoke directly in Go.
|
||||
func ExpectFunctionArg(vm *goja.Runtime, call goja.FunctionCall, index int) goja.Callable {
|
||||
arg := call.Argument(index)
|
||||
|
||||
if goja.IsUndefined(arg) {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d is missing", index)))
|
||||
}
|
||||
|
||||
fn, ok := goja.AssertFunction(arg)
|
||||
if !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Argument %d must be a function", index)))
|
||||
}
|
||||
|
||||
return fn
|
||||
}
|
||||
|
||||
// GetStringField extracts a string from an object.
|
||||
// If 'required' is true, it panics on missing keys or wrong types.
|
||||
func GetStringField(vm *goja.Runtime, obj *goja.Object, key string, required bool) string {
|
||||
val := obj.Get(key)
|
||||
|
||||
if goja.IsUndefined(val) || goja.IsNull(val) {
|
||||
if required {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Missing required field: '%s'", key)))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
strVal := val.String()
|
||||
// Strict type check
|
||||
if _, ok := val.Export().(string); !ok {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Field '%s' must be a string", key)))
|
||||
}
|
||||
|
||||
return strVal
|
||||
}
|
||||
|
||||
// GetIntField extracts an int from an object.
|
||||
func GetIntField(vm *goja.Runtime, obj *goja.Object, key string, required bool) int64 {
|
||||
val := obj.Get(key)
|
||||
|
||||
if goja.IsUndefined(val) || goja.IsNull(val) {
|
||||
if required {
|
||||
panic(vm.NewTypeError(fmt.Sprintf("Missing required field: '%s'", key)))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return val.ToInteger()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package goja_util
|
||||
package gojautil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package goja_util
|
||||
package gojautil
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package videocore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"seanime/internal/continuity"
|
||||
"seanime/internal/discordrpc/presence"
|
||||
"seanime/internal/events"
|
||||
@@ -15,7 +16,7 @@ func (vc *VideoCore) setupEffects() {
|
||||
}
|
||||
|
||||
func (vc *VideoCore) setupSharedEffects() {
|
||||
subscriber := vc.Subscribe("video-core:shared")
|
||||
subscriber := vc.Subscribe("videocore:shared")
|
||||
|
||||
go func(subscriber *Subscriber) {
|
||||
for e := range subscriber.Events() {
|
||||
@@ -52,6 +53,39 @@ func (vc *VideoCore) setupSharedEffects() {
|
||||
if vc.discordPresence != nil && !vc.isOfflineRef.Get() {
|
||||
go vc.discordPresence.Close()
|
||||
}
|
||||
case *VideoCompletedEvent:
|
||||
state, ok := vc.GetPlaybackState()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
shouldUpdateProgress := false
|
||||
vc.settingsMu.RLock()
|
||||
shouldUpdateProgress = vc.settings.Library.AutoUpdateProgress
|
||||
vc.settingsMu.RUnlock()
|
||||
if shouldUpdateProgress {
|
||||
// get the list entry
|
||||
collection, err := vc.platformRef.Get().GetAnimeCollection(context.Background(), false)
|
||||
if err != nil {
|
||||
vc.logger.Error().Err(err).Msg("videocore: Cannot update progress, failed to get anime collection")
|
||||
continue
|
||||
}
|
||||
mediaId := state.PlaybackInfo.Media.GetID()
|
||||
listEntry, ok := collection.GetListEntryFromAnimeId(mediaId)
|
||||
if !ok {
|
||||
vc.logger.Error().Msg("videocore: Cannot update progress, failed to get list entry for media")
|
||||
continue
|
||||
}
|
||||
progress := state.PlaybackInfo.Episode.GetProgressNumber()
|
||||
if listEntry.Progress != nil && progress <= *listEntry.Progress {
|
||||
continue
|
||||
}
|
||||
totalEpisodes := state.PlaybackInfo.Media.Episodes
|
||||
err = vc.platformRef.Get().UpdateEntryProgress(context.Background(), mediaId, progress, totalEpisodes)
|
||||
if err != nil {
|
||||
vc.logger.Error().Err(err).Msgf("videocore: Failed to update progress for media %d", mediaId)
|
||||
}
|
||||
vc.refreshAnimeCollectionFunc()
|
||||
}
|
||||
case *VideoTerminatedEvent:
|
||||
if vc.discordPresence != nil && !vc.isOfflineRef.Get() {
|
||||
go vc.discordPresence.Close()
|
||||
@@ -80,7 +114,7 @@ func (vc *VideoCore) setupSharedEffects() {
|
||||
}
|
||||
|
||||
func (vc *VideoCore) setupOnlinestreamEffects() {
|
||||
subscriber := vc.Subscribe("video-core:onlinestream")
|
||||
subscriber := vc.Subscribe("videocore:onlinestream")
|
||||
|
||||
go func(subscriber *Subscriber) {
|
||||
for e := range subscriber.Events() {
|
||||
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
PlayerEventVideoTerminated ClientEventType = "video-terminated"
|
||||
PlayerEventVideoPlaybackState ClientEventType = "video-playback-state"
|
||||
PlayerEventSubtitleFileUploaded ClientEventType = "subtitle-file-uploaded"
|
||||
PlayerEventVideoPlaylist ClientEventType = "video-playlist"
|
||||
PlayerEventVideoTextTracks ClientEventType = "video-text-tracks"
|
||||
)
|
||||
|
||||
type PlayerType string
|
||||
@@ -61,6 +63,13 @@ type VideoSubtitleTrack struct {
|
||||
UseLibassRenderer *bool `json:"useLibassRenderer"`
|
||||
}
|
||||
|
||||
type VideoTextTrack struct {
|
||||
Number int `json:"number"`
|
||||
Type string `json:"type"` // "subtitles" | "captions"
|
||||
Label string `json:"label"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
// VideoSource is an alternative video stream source (e.g., resolution options).
|
||||
type VideoSource struct {
|
||||
Index int `json:"index"`
|
||||
@@ -109,6 +118,16 @@ type VideoPlaybackInfo struct {
|
||||
IsNakamaWatchParty bool `json:"isNakamaWatchParty,omitempty"`
|
||||
}
|
||||
|
||||
// VideoPlaylistState holds the state for the video player's playlist and playback.
|
||||
type VideoPlaylistState struct {
|
||||
Type PlaybackType `json:"type"`
|
||||
Episodes []*anime.Episode `json:"episodes"`
|
||||
PreviousEpisode *anime.Episode `json:"previousEpisode,omitempty"`
|
||||
NextEpisode *anime.Episode `json:"nextEpisode,omitempty"`
|
||||
CurrentEpisode *anime.Episode `json:"currentEpisode"`
|
||||
AnimeEntry *anime.Entry `json:"animeEntry,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
PlaybackStatus struct {
|
||||
Id string `json:"id"`
|
||||
@@ -139,6 +158,9 @@ type (
|
||||
clientVideoLoadedPayload struct {
|
||||
State PlaybackState `json:"state"`
|
||||
}
|
||||
clientVideoPlaylistPayload struct {
|
||||
Playlist VideoPlaylistState `json:"playlist"`
|
||||
}
|
||||
clientVideoErrorPayload struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -170,6 +192,9 @@ type (
|
||||
clientVideoAnime4KPayload struct {
|
||||
Option string `json:"option"`
|
||||
}
|
||||
clientVideoTextTracksPayload struct {
|
||||
TextTracks []*VideoTextTrack `json:"textTracks"`
|
||||
}
|
||||
)
|
||||
|
||||
func (e *ClientEvent) UnmarshalAs(dest interface{}) error {
|
||||
@@ -324,6 +349,14 @@ type (
|
||||
BaseVideoEvent
|
||||
Option string `json:"string"` // name or "off"
|
||||
}
|
||||
VideoPlaylistEvent struct {
|
||||
BaseVideoEvent
|
||||
Playlist *VideoPlaylistState `json:"playlist"`
|
||||
}
|
||||
VideoTextTracksEvent struct {
|
||||
BaseVideoEvent
|
||||
TextTracks []*VideoTextTrack `json:"textTracks"`
|
||||
}
|
||||
)
|
||||
|
||||
func (e *VideoStatusEvent) IsCritical() bool { return false }
|
||||
@@ -349,7 +382,10 @@ const (
|
||||
ServerEventTerminate ServerEvent = "terminate"
|
||||
ServerEventStartOnlinestreamWatchParty ServerEvent = "start-onlinestream-watch-party"
|
||||
ServerEventGetStatus ServerEvent = "get-status"
|
||||
// Getters
|
||||
ServerEventShowMessage ServerEvent = "show-message"
|
||||
ServerEventPlayEpisode ServerEvent = "play-episode"
|
||||
ServerEventGetTextTracks ServerEvent = "get-text-tracks"
|
||||
// State requests
|
||||
ServerEventGetFullscreen ServerEvent = "get-fullscreen"
|
||||
ServerEventGetPip ServerEvent = "get-pip"
|
||||
ServerEventGetAnime4K ServerEvent = "get-anime-4k"
|
||||
@@ -357,4 +393,5 @@ const (
|
||||
ServerEventGetAudioTrack ServerEvent = "get-audio-track"
|
||||
ServerEventGetMediaCaptionTrack ServerEvent = "get-media-caption-track"
|
||||
ServerEventGetPlaybackState ServerEvent = "get-playback-state"
|
||||
ServerEventGetPlaylist ServerEvent = "get-playlist"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/metadata_provider"
|
||||
"seanime/internal/continuity"
|
||||
"seanime/internal/database/models"
|
||||
discordrpc_presence "seanime/internal/discordrpc/presence"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/mkvparser"
|
||||
@@ -44,11 +45,14 @@ type (
|
||||
dispatcherStop chan struct{}
|
||||
startOnce sync.Once
|
||||
|
||||
logger *zerolog.Logger
|
||||
logger *zerolog.Logger
|
||||
settingsMu sync.RWMutex
|
||||
settings *models.Settings
|
||||
}
|
||||
|
||||
// Subscriber listens to the player events
|
||||
Subscriber struct {
|
||||
id string
|
||||
eventCh chan VideoEvent
|
||||
isClosed atomic.Bool
|
||||
closeOnce sync.Once
|
||||
@@ -86,6 +90,12 @@ func New(opts NewVideoCoreOptions) *VideoCore {
|
||||
return vc
|
||||
}
|
||||
|
||||
func (vc *VideoCore) SetSettings(settings *models.Settings) {
|
||||
vc.settingsMu.Lock()
|
||||
vc.settings = settings
|
||||
vc.settingsMu.Unlock()
|
||||
}
|
||||
|
||||
func (vc *VideoCore) Start() {
|
||||
vc.startOnce.Do(func() {
|
||||
vc.listenToClientEvents()
|
||||
@@ -191,6 +201,7 @@ func (vc *VideoCore) sendPlayerEvent(t string, payload interface{}) {
|
||||
// Subscribe lets other modules subscribe to the native player events
|
||||
func (vc *VideoCore) Subscribe(id string) *Subscriber {
|
||||
subscriber := &Subscriber{
|
||||
id: id,
|
||||
eventCh: make(chan VideoEvent, 100),
|
||||
}
|
||||
vc.subscribers.Set(id, subscriber)
|
||||
@@ -213,6 +224,11 @@ func (s *Subscriber) Events() <-chan VideoEvent {
|
||||
return s.eventCh
|
||||
}
|
||||
|
||||
// GetId returns the subscriber id
|
||||
func (s *Subscriber) GetId() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
func (vc *VideoCore) RegisterEventCallback(callback func(event VideoEvent) bool) (cancel func()) {
|
||||
id := uuid.NewString()
|
||||
sub := vc.Subscribe(id)
|
||||
@@ -460,6 +476,24 @@ func (vc *VideoCore) SetAudioTrack(trackNumber int) {
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventSetAudioTrack), trackNumber)
|
||||
}
|
||||
|
||||
func (vc *VideoCore) ShowMessage(message string) {
|
||||
state, ok := vc.GetPlaybackState()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventShowMessage), message)
|
||||
}
|
||||
|
||||
// PlayEpisode sends a play-episode command to the video player.
|
||||
// which is "next", "previous", or the AniDB episode ID.
|
||||
func (vc *VideoCore) PlayEpisode(which string) {
|
||||
state, ok := vc.GetPlaybackState()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventPlayEpisode), which)
|
||||
}
|
||||
|
||||
// Terminate sends a terminate command to the video player.
|
||||
func (vc *VideoCore) Terminate() {
|
||||
state, ok := vc.GetPlaybackState()
|
||||
@@ -538,6 +572,56 @@ func (vc *VideoCore) SendGetPlaybackState() {
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventGetPlaybackState), nil)
|
||||
}
|
||||
|
||||
// GetPlaylist sends a get-text-tracks request to the video player and returns the text tracks.
|
||||
func (vc *VideoCore) GetTextTracks() (ret []*VideoTextTrack, ok bool) {
|
||||
state, ok := vc.GetPlaybackState()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
done := make(chan struct{})
|
||||
cancel := vc.RegisterEventCallback(func(e VideoEvent) bool {
|
||||
switch event := e.(type) {
|
||||
case *VideoTextTracksEvent:
|
||||
ret = event.TextTracks
|
||||
close(done)
|
||||
return false // stop
|
||||
}
|
||||
return true // keep listening
|
||||
})
|
||||
go func(cancel func()) {
|
||||
defer cancel()
|
||||
<-time.After(5 * time.Second)
|
||||
}(cancel)
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventGetTextTracks), nil)
|
||||
<-done
|
||||
return ret, ret != nil
|
||||
}
|
||||
|
||||
// GetPlaylist sends a get-playlist request to the video player and returns the playlist state.
|
||||
func (vc *VideoCore) GetPlaylist() (ret *VideoPlaylistState, ok bool) {
|
||||
state, ok := vc.GetPlaybackState()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
done := make(chan struct{})
|
||||
cancel := vc.RegisterEventCallback(func(e VideoEvent) bool {
|
||||
switch event := e.(type) {
|
||||
case *VideoPlaylistEvent:
|
||||
ret = event.Playlist
|
||||
close(done)
|
||||
return false // stop
|
||||
}
|
||||
return true // keep listening
|
||||
})
|
||||
go func(cancel func()) {
|
||||
defer cancel()
|
||||
<-time.After(5 * time.Second)
|
||||
}(cancel)
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventGetPlaylist), nil)
|
||||
<-done
|
||||
return ret, ret != nil
|
||||
}
|
||||
|
||||
// PullStatus pulls the current playback status from the video player.
|
||||
func (vc *VideoCore) PullStatus() (ret VideoStatusEvent, ok bool) {
|
||||
state, ok := vc.GetPlaybackState()
|
||||
@@ -545,7 +629,7 @@ func (vc *VideoCore) PullStatus() (ret VideoStatusEvent, ok bool) {
|
||||
return VideoStatusEvent{}, false
|
||||
}
|
||||
done := make(chan struct{})
|
||||
vc.RegisterEventCallback(func(e VideoEvent) bool {
|
||||
cancel := vc.RegisterEventCallback(func(e VideoEvent) bool {
|
||||
switch event := e.(type) {
|
||||
case *VideoStatusEvent:
|
||||
ret = *event
|
||||
@@ -554,6 +638,10 @@ func (vc *VideoCore) PullStatus() (ret VideoStatusEvent, ok bool) {
|
||||
}
|
||||
return true // keep listening
|
||||
})
|
||||
go func(cancel func()) {
|
||||
defer cancel()
|
||||
<-time.After(5 * time.Second)
|
||||
}(cancel)
|
||||
vc.sendPlayerEventTo(state.ClientId, string(ServerEventGetStatus), nil)
|
||||
<-done
|
||||
return ret, true
|
||||
@@ -760,6 +848,20 @@ func (vc *VideoCore) listenToClientEvents() {
|
||||
Content: payload.Content,
|
||||
})
|
||||
}
|
||||
case PlayerEventVideoPlaylist:
|
||||
payload := &clientVideoPlaylistPayload{}
|
||||
if err := playerEvent.UnmarshalAs(payload); err == nil {
|
||||
vc.PushEvent(&VideoPlaylistEvent{
|
||||
Playlist: &payload.Playlist,
|
||||
})
|
||||
}
|
||||
case PlayerEventVideoTextTracks:
|
||||
payload := &clientVideoTextTracksPayload{}
|
||||
if err := playerEvent.UnmarshalAs(payload); err == nil {
|
||||
vc.PushEvent(&VideoTextTracksEvent{
|
||||
TextTracks: payload.TextTracks,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4758,7 +4758,9 @@ export type VideoCore_ClientEventType = "video-loaded" |
|
||||
"video-error" |
|
||||
"video-terminated" |
|
||||
"video-playback-state" |
|
||||
"subtitle-file-uploaded"
|
||||
"subtitle-file-uploaded" |
|
||||
"video-playlist" |
|
||||
"video-text-tracks"
|
||||
|
||||
/**
|
||||
* - Filepath: internal/videocore/types.go
|
||||
@@ -4821,13 +4823,17 @@ export type VideoCore_ServerEvent = "pause" |
|
||||
"terminate" |
|
||||
"start-onlinestream-watch-party" |
|
||||
"get-status" |
|
||||
"show-message" |
|
||||
"play-episode" |
|
||||
"get-text-tracks" |
|
||||
"get-fullscreen" |
|
||||
"get-pip" |
|
||||
"get-anime-4k" |
|
||||
"get-subtitle-track" |
|
||||
"get-audio-track" |
|
||||
"get-media-caption-track" |
|
||||
"get-playback-state"
|
||||
"get-playback-state" |
|
||||
"get-playlist"
|
||||
|
||||
/**
|
||||
* - Filepath: internal/videocore/types.go
|
||||
|
||||
@@ -226,7 +226,7 @@ export function NakamaManager() {
|
||||
|
||||
const { startOnlineStreamWatchParty } = useNakamaOnlineStreamWatchParty()
|
||||
useWebsocketMessageListener({
|
||||
type: WSEvents.VIDEO_CORE,
|
||||
type: WSEvents.VIDEOCORE,
|
||||
onMessage: ({ type, payload }: { type: VideoCore_ServerEvent, payload: unknown }) => {
|
||||
switch (type) {
|
||||
case "start-onlinestream-watch-party":
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
vc_mediaCaptionsManager,
|
||||
vc_subtitleManager,
|
||||
} from "@/app/(main)/_features/video-core/video-core"
|
||||
import { vc_doFlashAction } from "@/app/(main)/_features/video-core/video-core-action-display"
|
||||
import { Anime4KManagerOptionChangedEvent } from "@/app/(main)/_features/video-core/video-core-anime-4k-manager"
|
||||
import { AudioManagerHlsTrackChangedEvent, AudioManagerTrackChangedEvent } from "@/app/(main)/_features/video-core/video-core-audio"
|
||||
import { FullscreenManagerChangedEvent, vc_fullscreenManager } from "@/app/(main)/_features/video-core/video-core-fullscreen"
|
||||
@@ -15,14 +16,15 @@ import {
|
||||
MediaCaptionsTrackSelectedEvent,
|
||||
} from "@/app/(main)/_features/video-core/video-core-media-captions"
|
||||
import { PipManagerToggledEvent, vc_pipManager } from "@/app/(main)/_features/video-core/video-core-pip"
|
||||
import { useVideoCorePlaylist, VideoCorePlaylistState } from "@/app/(main)/_features/video-core/video-core-playlist"
|
||||
import { SubtitleManagerTrackDeselectedEvent, SubtitleManagerTrackSelectedEvent } from "@/app/(main)/_features/video-core/video-core-subtitles"
|
||||
import { vc_autoNextAtom, VideoCoreLifecycleState, VideoCore_VideoSubtitleTrack } from "@/app/(main)/_features/video-core/video-core.atoms"
|
||||
import { vc_autoNextAtom, VideoCore_VideoSubtitleTrack, VideoCoreLifecycleState } from "@/app/(main)/_features/video-core/video-core.atoms"
|
||||
import { detectSubtitleType, isSubtitleFile } from "@/app/(main)/_features/video-core/video-core.utils"
|
||||
import { useWebsocketMessageListener, useWebsocketSender } from "@/app/(main)/_hooks/handle-websockets"
|
||||
import { clientIdAtom } from "@/app/websocket-provider"
|
||||
import { logger } from "@/lib/helpers/debug"
|
||||
import { WSEvents } from "@/lib/server/ws-events"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { useAtomValue, useSetAtom } from "jotai"
|
||||
import React, { useCallback, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -96,6 +98,8 @@ export function useVideoCoreSetupEvents(id: string,
|
||||
const pipManager = useAtomValue(vc_pipManager)
|
||||
const audioManager = useAtomValue(vc_audioManager)
|
||||
const autoNext = useAtomValue(vc_autoNextAtom)
|
||||
const flashAction = useSetAtom(vc_doFlashAction)
|
||||
const { playEpisode, playlistState } = useVideoCorePlaylist()
|
||||
|
||||
// React.useEffect(() => {
|
||||
// log.trace(activePlayer, id)
|
||||
@@ -477,7 +481,7 @@ export function useVideoCoreSetupEvents(id: string,
|
||||
}, [handleUpload, state.active, videoRef.current])
|
||||
|
||||
useWebsocketMessageListener({
|
||||
type: WSEvents.VIDEO_CORE,
|
||||
type: WSEvents.VIDEOCORE,
|
||||
deps: [activePlayer, id],
|
||||
onMessage: ({ type, payload }: { type: VideoCore_ServerEvent, payload: unknown }) => {
|
||||
if (activePlayer !== id || !videoRef.current) return
|
||||
@@ -559,9 +563,9 @@ export function useVideoCoreSetupEvents(id: string,
|
||||
case "add-external-subtitle-track":
|
||||
log.info("Add subtitle track event received", payload)
|
||||
const fileTrack = payload as VideoCore_VideoSubtitleTrack
|
||||
if (subtitleManager && fileTrack.type === "ass") {
|
||||
if (subtitleManager) {
|
||||
subtitleManager.addFileTrack(fileTrack)
|
||||
} else if (mediaCaptionsManager && fileTrack.type === "vtt") {
|
||||
} else if (mediaCaptionsManager) {
|
||||
mediaCaptionsManager.addCaptionTrack(fileTrack)
|
||||
}
|
||||
break
|
||||
@@ -640,6 +644,43 @@ export function useVideoCoreSetupEvents(id: string,
|
||||
},
|
||||
})
|
||||
break
|
||||
case "show-message":
|
||||
log.info("Show message event received", payload)
|
||||
flashAction({ message: payload as string, type: "message", duration: 2000 })
|
||||
break
|
||||
case "get-playlist":
|
||||
log.info("Get playlist event received")
|
||||
if (!playlistState) return
|
||||
sendEvent<{ playlist: VideoCorePlaylistState }>("video-playlist", {
|
||||
playlist: playlistState,
|
||||
})
|
||||
break
|
||||
case "play-episode":
|
||||
log.info("Play next episode event received")
|
||||
playEpisode(payload as string)
|
||||
break
|
||||
case "start-onlinestream-watch-party":
|
||||
break
|
||||
case "get-text-tracks":
|
||||
log.info("Get text tracks event received")
|
||||
let textTracks: { type: "subtitles" | "captions", label: string, language: string, number: number }[] = []
|
||||
if (subtitleManager) {
|
||||
textTracks = subtitleManager.getTracks().map(n => ({
|
||||
number: n.number,
|
||||
type: "subtitles",
|
||||
label: n.label || "",
|
||||
language: n.language || n.languageIETF || "",
|
||||
}))
|
||||
} else if (mediaCaptionsManager) {
|
||||
textTracks = mediaCaptionsManager.getTracks().map(n => ({
|
||||
number: n.number,
|
||||
type: "captions",
|
||||
label: n.label,
|
||||
language: n.language,
|
||||
}))
|
||||
}
|
||||
sendEvent("video-text-tracks", { textTracks })
|
||||
break
|
||||
default:
|
||||
log.warn("Unknown event received", type)
|
||||
}
|
||||
@@ -661,7 +702,7 @@ export function useVideoCoreEvents() {
|
||||
|
||||
function sendEvent<T extends Record<string, any> | void = void>(type: VideoCore_ClientEventType, payload?: T) {
|
||||
sendMessage({
|
||||
type: WSEvents.VIDEO_CORE,
|
||||
type: WSEvents.VIDEOCORE,
|
||||
payload: {
|
||||
clientId: clientId,
|
||||
type: type,
|
||||
|
||||
@@ -38,16 +38,11 @@ export function VideoCoreInlineHelpers({
|
||||
const [hasUpdatedProgress, setHasUpdateProgress] = useAtom(vc_inlineHelper_hasUpdatedProgress)
|
||||
const [progressUpdateData, setProgressUpdateData] = useAtom(vc_inlineHelper_progressUpdateData)
|
||||
|
||||
const { mutate: updateProgress, isPending: isUpdatingProgress, isSuccess: updated } = useUpdateAnimeEntryProgress(
|
||||
media?.id,
|
||||
currentProgress,
|
||||
)
|
||||
|
||||
// Reset state when media, episode, or update completes
|
||||
React.useEffect(() => {
|
||||
setProgressUpdateData(null)
|
||||
setHasUpdateProgress(false)
|
||||
}, [media, currentEpisodeNumber, url, updated])
|
||||
}, [media, currentEpisodeNumber, url])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!playerRef.current || !media || currentEpisodeNumber === null || !url) return
|
||||
@@ -58,10 +53,10 @@ export function VideoCoreInlineHelpers({
|
||||
|
||||
const checkProgress = () => {
|
||||
const player = playerRef.current
|
||||
if (!player) return
|
||||
if (!player || serverStatus?.settings?.library?.autoUpdateProgress) return
|
||||
|
||||
// Skip if already updated or currently updating
|
||||
if (hasUpdatedProgress || isUpdatingProgress) return
|
||||
if (hasUpdatedProgress) return
|
||||
|
||||
// Skip if progress update data already exists
|
||||
if (progressUpdateData !== null) return
|
||||
@@ -75,29 +70,12 @@ export function VideoCoreInlineHelpers({
|
||||
const watchedRatio = currentTime / duration
|
||||
if (watchedRatio < PROGRESS_THRESHOLD) return
|
||||
|
||||
// Handle auto-update or prompt user
|
||||
if (serverStatus?.settings?.library?.autoUpdateProgress) {
|
||||
setHasUpdateProgress(true)
|
||||
updateProgress({
|
||||
episodeNumber: currentEpisodeNumber,
|
||||
mediaId: media.id,
|
||||
totalEpisodes: media.episodes || 0,
|
||||
malId: media.idMal || undefined,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setHasUpdateProgress(true)
|
||||
},
|
||||
onError: () => {
|
||||
setHasUpdateProgress(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setProgressUpdateData({
|
||||
media,
|
||||
currentProgress,
|
||||
currentEpisodeNumber,
|
||||
})
|
||||
}
|
||||
// prompt user
|
||||
setProgressUpdateData({
|
||||
media,
|
||||
currentProgress,
|
||||
currentEpisodeNumber,
|
||||
})
|
||||
}
|
||||
|
||||
// Start interval
|
||||
@@ -112,7 +90,6 @@ export function VideoCoreInlineHelpers({
|
||||
media,
|
||||
playerRef,
|
||||
hasUpdatedProgress,
|
||||
isUpdatingProgress,
|
||||
serverStatus?.settings?.library?.autoUpdateProgress,
|
||||
currentProgress,
|
||||
progressUpdateData,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { vc_getCaptionStyle } from "@/app/(main)/_features/video-core/video-core
|
||||
import { getDefaultSubtitleTrackNumber } from "@/app/(main)/_features/video-core/video-core-subtitles"
|
||||
import { VideoCoreSettings } from "@/app/(main)/_features/video-core/video-core.atoms"
|
||||
import { logger } from "@/lib/helpers/debug"
|
||||
import { CaptionsRenderer, ParsedCaptionsResult, parseResponse, parseText, VTTCue, VTTRegion } from "media-captions"
|
||||
import { CaptionsFileFormat, CaptionsRenderer, ParsedCaptionsResult, parseResponse, parseText, VTTCue, VTTRegion } from "media-captions"
|
||||
import "media-captions/styles/captions.css"
|
||||
import "media-captions/styles/regions.css"
|
||||
import { toast } from "sonner"
|
||||
@@ -429,7 +429,7 @@ export class MediaCaptionsManager extends EventTarget {
|
||||
// Adds a new subtitle track and selects it AFTER initialization
|
||||
// This is used for adding subtitles from the server
|
||||
public addCaptionTrack(track: MediaCaptionsTrackInfo) {
|
||||
toast.success(`Subtitle track added: ${track.label}`)
|
||||
toast.success(`Caption track added: ${track.label}`)
|
||||
this.tracks.push(track)
|
||||
const index = this.tracks.length - 1
|
||||
this.loadedTracks.push({
|
||||
@@ -442,7 +442,7 @@ export class MediaCaptionsManager extends EventTarget {
|
||||
if (track.src) {
|
||||
return await parseResponse(fetch(track.src))
|
||||
} else if (track.content) {
|
||||
return await parseText(track.content, { type: "vtt" })
|
||||
return await parseText(track.content, { type: track.type as CaptionsFileFormat || "vtt" })
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Anime_Entry, Anime_Episode, HibikeTorrent_AnimeTorrent } from "@/api/generated/types"
|
||||
import { Anime_Entry, Anime_Episode } from "@/api/generated/types"
|
||||
import { useGetAnimeEpisodeCollection } from "@/api/hooks/anime.hooks"
|
||||
import { useGetAnimeEntry } from "@/api/hooks/anime_entries.hooks"
|
||||
import { EpisodeGridItem } from "@/app/(main)/_features/anime/_components/episode-grid-item"
|
||||
@@ -31,13 +31,12 @@ import React from "react"
|
||||
import { useUpdateEffect } from "react-use"
|
||||
import { toast } from "sonner"
|
||||
|
||||
type VideoCorePlaylistState = {
|
||||
export type VideoCorePlaylistState = {
|
||||
type: VideoCore_PlaybackType
|
||||
episodes: Anime_Episode[]
|
||||
previousEpisode: Anime_Episode | null
|
||||
nextEpisode: Anime_Episode | null
|
||||
currentEpisode: Anime_Episode
|
||||
currentTorrent?: HibikeTorrent_AnimeTorrent // for torrent and debrid stream type
|
||||
animeEntry: Anime_Entry | null
|
||||
onPlayEpisode?: VideoCorePlaylistPlayEpisodeFunction
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ export function OnlinestreamPage({ animeEntry, animeEntryLoading, hideBackButton
|
||||
const episodeLoading = isLoadingEpisodeSource || isFetchingEpisodeSource
|
||||
|
||||
const isWatchPartyPeer = React.useMemo(() => {
|
||||
return !!nakamaStatus?.currentWatchPartySession && !nakamaStatus.isHost && !nakamaStatus.currentWatchPartySession?.participants?.[nakamaStatus?.hostConnectionStatus?.peerId || ""]?.isRelayOrigin
|
||||
return !!nakamaStatus?.hostConnectionStatus && !!nakamaStatus?.currentWatchPartySession && !nakamaStatus.isHost && !nakamaStatus.currentWatchPartySession?.participants?.[nakamaStatus?.hostConnectionStatus?.peerId || ""]?.isRelayOrigin
|
||||
}, [nakamaStatus])
|
||||
|
||||
/*
|
||||
@@ -339,6 +339,7 @@ export function OnlinestreamPage({ animeEntry, animeEntryLoading, hideBackButton
|
||||
*/
|
||||
const firstRenderRef = React.useRef(true)
|
||||
React.useEffect(() => {
|
||||
console.warn(nakamaStatus)
|
||||
// Do not auto set the episode number if the user is in a watch party and is not the host
|
||||
if (isWatchPartyPeer) return
|
||||
|
||||
@@ -361,28 +362,6 @@ export function OnlinestreamPage({ animeEntry, animeEntryLoading, hideBackButton
|
||||
}
|
||||
}, [episodes, media, animeEntry?.listData, urlEpNumber, currentPlaylist, isWatchPartyPeer])
|
||||
|
||||
/*
|
||||
* Set episode number on update
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
// Do not auto set the episode number if the user is in a watch party and is not the host
|
||||
if (isWatchPartyPeer) return
|
||||
|
||||
// Do not auto set if we're loading from watch party
|
||||
if (isLoadingFromWatchPartyRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
if (firstRenderRef.current) return
|
||||
|
||||
if (!!media && !!episodes) {
|
||||
const episodeNumberFromURL = urlEpNumber ? Number(urlEpNumber) : undefined
|
||||
if (episodeNumberFromURL) {
|
||||
setSelectedEpisodeNumber(episodeNumberFromURL)
|
||||
log.info("Changing episode number to", episodeNumberFromURL)
|
||||
}
|
||||
}
|
||||
}, [urlEpNumber, isWatchPartyPeer])
|
||||
|
||||
function onCanPlay() {
|
||||
if (urlEpNumber) {
|
||||
|
||||
@@ -43,7 +43,7 @@ export const enum WSEvents {
|
||||
CONSOLE_LOG = "console-log",
|
||||
CONSOLE_WARN = "console-warn",
|
||||
NATIVE_PLAYER = "native-player",
|
||||
VIDEO_CORE = "video-core",
|
||||
VIDEOCORE = "videocore",
|
||||
NAKAMA_HOST_STARTED = "nakama-host-started",
|
||||
NAKAMA_HOST_STOPPED = "nakama-host-stopped",
|
||||
NAKAMA_PEER_CONNECTED = "nakama-peer-connected",
|
||||
|
||||
Reference in New Issue
Block a user