This commit is contained in:
5rahim
2025-12-03 15:46:40 +01:00
parent 0c4ef36fd5
commit d10cd9641e
24 changed files with 938 additions and 78 deletions

View File

@@ -6622,6 +6622,131 @@
]
}
},
{
"package": "platform",
"goStruct": {
"filepath": "../internal/platforms/platform/hook_events.go",
"filename": "hook_events.go",
"name": "PreDeleteEntryEvent",
"formattedName": "PreDeleteEntryEvent",
"package": "platform",
"fields": [
{
"name": "MediaID",
"jsonName": "mediaId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
},
{
"name": "EntryID",
"jsonName": "entryId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
},
{
"name": "next",
"jsonName": "next",
"goType": "",
"typescriptType": "any",
"required": true,
"public": false,
"comments": []
},
{
"name": "preventDefault",
"jsonName": "preventDefault",
"goType": "",
"typescriptType": "any",
"required": true,
"public": false,
"comments": []
},
{
"name": "DefaultPrevented",
"jsonName": "defaultPrevented",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
}
],
"comments": [
" PreDeleteEntryEvent is triggered when an entry is about to be deleted.",
" Prevent default to skip the default deletion and override the deletion."
],
"embeddedStructNames": [
"hook_resolver.Event"
]
}
},
{
"package": "platform",
"goStruct": {
"filepath": "../internal/platforms/platform/hook_events.go",
"filename": "hook_events.go",
"name": "PostDeleteEntryEvent",
"formattedName": "PostDeleteEntryEvent",
"package": "platform",
"fields": [
{
"name": "MediaID",
"jsonName": "mediaId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
},
{
"name": "EntryID",
"jsonName": "entryId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
},
{
"name": "next",
"jsonName": "next",
"goType": "",
"typescriptType": "any",
"required": true,
"public": false,
"comments": []
},
{
"name": "preventDefault",
"jsonName": "preventDefault",
"goType": "",
"typescriptType": "any",
"required": true,
"public": false,
"comments": []
},
{
"name": "DefaultPrevented",
"jsonName": "defaultPrevented",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
}
],
"comments": [],
"embeddedStructNames": [
"hook_resolver.Event"
]
}
},
{
"package": "torrentstream",
"goStruct": {

View File

@@ -42275,6 +42275,63 @@
],
"comments": []
},
{
"filepath": "../internal/hook/helper.go",
"filename": "helper.go",
"name": "HookTriggerOptions",
"formattedName": "HookTriggerOptions",
"package": "hook",
"fields": [
{
"name": "Event",
"jsonName": "Event",
"goType": "T",
"typescriptType": "T",
"usedTypescriptType": "T",
"usedStructName": "hook.T",
"required": true,
"public": true,
"comments": []
},
{
"name": "Hook",
"jsonName": "Hook",
"goType": "",
"typescriptType": "any",
"required": true,
"public": true,
"comments": []
},
{
"name": "OnError",
"jsonName": "OnError",
"goType": "",
"typescriptType": "any",
"required": true,
"public": true,
"comments": []
},
{
"name": "OnDefaultPrevented",
"jsonName": "OnDefaultPrevented",
"goType": "",
"typescriptType": "any",
"required": true,
"public": true,
"comments": []
},
{
"name": "OnSuccess",
"jsonName": "OnSuccess",
"goType": "",
"typescriptType": "any",
"required": true,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/hook/hook.go",
"filename": "hook.go",
@@ -42555,6 +42612,24 @@
"public": false,
"comments": []
},
{
"name": "onPreDeleteEntry",
"jsonName": "onPreDeleteEntry",
"goType": "",
"typescriptType": "any",
"required": false,
"public": false,
"comments": []
},
{
"name": "onPostDeleteEntry",
"jsonName": "onPostDeleteEntry",
"goType": "",
"typescriptType": "any",
"required": false,
"public": false,
"comments": []
},
{
"name": "onAnimeEntryRequested",
"jsonName": "onAnimeEntryRequested",
@@ -66322,6 +66397,391 @@
],
"comments": []
},
{
"filepath": "../internal/pgs/pgs.go",
"filename": "pgs.go",
"name": "PgsDecoder",
"formattedName": "PgsDecoder",
"package": "pgs",
"fields": [
{
"name": "Palette",
"jsonName": "Palette",
"goType": "color.Palette",
"typescriptType": "Palette",
"usedTypescriptType": "Palette",
"usedStructName": "color.Palette",
"required": false,
"public": true,
"comments": []
},
{
"name": "currentObject",
"jsonName": "currentObject",
"goType": "PgsObject",
"typescriptType": "PgsObject",
"usedTypescriptType": "PgsObject",
"usedStructName": "pgs.PgsObject",
"required": false,
"public": false,
"comments": []
},
{
"name": "currentComposition",
"jsonName": "currentComposition",
"goType": "PgsComposition",
"typescriptType": "PgsComposition",
"usedTypescriptType": "PgsComposition",
"usedStructName": "pgs.PgsComposition",
"required": false,
"public": false,
"comments": []
},
{
"name": "objects",
"jsonName": "objects",
"goType": "map[uint16]PgsObject",
"typescriptType": "Record\u003cnumber, PgsObject\u003e",
"usedTypescriptType": "PgsObject",
"usedStructName": "pgs.PgsObject",
"required": false,
"public": false,
"comments": [
" Store completed objects by ID"
]
},
{
"name": "windows",
"jsonName": "windows",
"goType": "map[uint8]WindowDefinition",
"typescriptType": "Record\u003cnumber, WindowDefinition\u003e",
"usedTypescriptType": "WindowDefinition",
"usedStructName": "pgs.WindowDefinition",
"required": false,
"public": false,
"comments": []
}
],
"comments": [
" PgsDecoder decodes PGS packets into images"
]
},
{
"filepath": "../internal/pgs/pgs.go",
"filename": "pgs.go",
"name": "PgsObject",
"formattedName": "PgsObject",
"package": "pgs",
"fields": [
{
"name": "ID",
"jsonName": "ID",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Version",
"jsonName": "Version",
"goType": "uint8",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Width",
"jsonName": "Width",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Height",
"jsonName": "Height",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Data",
"jsonName": "Data",
"goType": "string",
"typescriptType": "Array\u003cstring\u003e",
"required": false,
"public": true,
"comments": []
},
{
"name": "Complete",
"jsonName": "Complete",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/pgs/pgs.go",
"filename": "pgs.go",
"name": "PgsComposition",
"formattedName": "PgsComposition",
"package": "pgs",
"fields": [
{
"name": "PTS",
"jsonName": "PTS",
"goType": "uint64",
"typescriptType": "number",
"required": true,
"public": true,
"comments": [
" Presentation timestamp"
]
},
{
"name": "DTS",
"jsonName": "DTS",
"goType": "uint64",
"typescriptType": "number",
"required": true,
"public": true,
"comments": [
" Decode timestamp"
]
},
{
"name": "Width",
"jsonName": "Width",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Height",
"jsonName": "Height",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "FrameRate",
"jsonName": "FrameRate",
"goType": "uint8",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "CompositionNum",
"jsonName": "CompositionNum",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "CompositionState",
"jsonName": "CompositionState",
"goType": "uint8",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "PaletteUpdate",
"jsonName": "PaletteUpdate",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
},
{
"name": "PaletteID",
"jsonName": "PaletteID",
"goType": "uint8",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Objects",
"jsonName": "Objects",
"goType": "[]CompositionObject",
"typescriptType": "Array\u003cCompositionObject\u003e",
"usedTypescriptType": "CompositionObject",
"usedStructName": "pgs.CompositionObject",
"required": false,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/pgs/pgs.go",
"filename": "pgs.go",
"name": "CompositionObject",
"formattedName": "CompositionObject",
"package": "pgs",
"fields": [
{
"name": "ObjectID",
"jsonName": "ObjectID",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "WindowID",
"jsonName": "WindowID",
"goType": "uint8",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "X",
"jsonName": "X",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Y",
"jsonName": "Y",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Cropped",
"jsonName": "Cropped",
"goType": "bool",
"typescriptType": "boolean",
"required": true,
"public": true,
"comments": []
},
{
"name": "CropX",
"jsonName": "CropX",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "CropY",
"jsonName": "CropY",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "CropWidth",
"jsonName": "CropWidth",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "CropHeight",
"jsonName": "CropHeight",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/pgs/pgs.go",
"filename": "pgs.go",
"name": "WindowDefinition",
"formattedName": "WindowDefinition",
"package": "pgs",
"fields": [
{
"name": "WindowID",
"jsonName": "WindowID",
"goType": "uint8",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "X",
"jsonName": "X",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Y",
"jsonName": "Y",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Width",
"jsonName": "Width",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
},
{
"name": "Height",
"jsonName": "Height",
"goType": "uint16",
"typescriptType": "number",
"required": true,
"public": true,
"comments": []
}
],
"comments": []
},
{
"filepath": "../internal/platforms/anilist_platform/anilist_platform.go",
"filename": "anilist_platform.go",
@@ -67034,6 +67494,71 @@
"hook_resolver.Event"
]
},
{
"filepath": "../internal/platforms/platform/hook_events.go",
"filename": "hook_events.go",
"name": "PreDeleteEntryEvent",
"formattedName": "PreDeleteEntryEvent",
"package": "platform",
"fields": [
{
"name": "MediaID",
"jsonName": "mediaId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
},
{
"name": "EntryID",
"jsonName": "entryId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
}
],
"comments": [
" PreDeleteEntryEvent is triggered when an entry is about to be deleted.",
" Prevent default to skip the default deletion and override the deletion."
],
"embeddedStructNames": [
"hook_resolver.Event"
]
},
{
"filepath": "../internal/platforms/platform/hook_events.go",
"filename": "hook_events.go",
"name": "PostDeleteEntryEvent",
"formattedName": "PostDeleteEntryEvent",
"package": "platform",
"fields": [
{
"name": "MediaID",
"jsonName": "mediaId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
},
{
"name": "EntryID",
"jsonName": "entryId",
"goType": "int",
"typescriptType": "number",
"required": false,
"public": true,
"comments": []
}
],
"comments": [],
"embeddedStructNames": [
"hook_resolver.Event"
]
},
{
"filepath": "../internal/platforms/shared_platform/cachelayer.go",
"filename": "cachelayer.go",

View File

@@ -51,6 +51,7 @@ type (
GetAnimeMetadataWrapper(anime *anilist.BaseAnime, metadata *metadata.AnimeMetadata) AnimeMetadataWrapper
GetCache() *result.BoundedCache[string, *metadata.AnimeMetadata]
SetUseFallbackProvider(bool)
ClearCache()
Close()
}
@@ -87,6 +88,10 @@ func (p *ProviderImpl) Close() {
go p.animeMetadataCache.Clear()
}
func (p *ProviderImpl) ClearCache() {
p.animeMetadataCache.Clear()
}
// GetCache returns the anime metadata cache.
func (p *ProviderImpl) GetCache() *result.BoundedCache[string, *metadata.AnimeMetadata] {
return p.animeMetadataCache

View File

@@ -33,15 +33,15 @@ func GetSeanimeFlags() SeanimeFlags {
fmt.Printf("Usage: seanime [flags]\n\n")
}
fmt.Printf("Flags:\n")
fmt.Printf(" -datadir string directory that contains all Seanime data\n")
fmt.Printf(" -host string host address to bind to (default: 127.0.0.1)\n")
fmt.Printf(" -port int port to bind to (default: 43211)\n")
fmt.Printf(" -update update the application\n")
fmt.Printf(" -desktop-sidecar run as the desktop sidecar\n")
fmt.Printf(" -disable-features string comma-separated list of features to disable\n")
fmt.Printf(" -disable-all-features disable all features that can be disabled\n")
fmt.Printf(" -password string password to use for the instance\n")
fmt.Printf(" -disable-password disable password protection\n")
fmt.Printf(" --datadir string directory that contains all Seanime data\n")
fmt.Printf(" --host string host address to bind to (default: 127.0.0.1)\n")
fmt.Printf(" --port int port to bind to (default: 43211)\n")
fmt.Printf(" --update update the application\n")
fmt.Printf(" --desktop-sidecar run as the desktop sidecar\n")
fmt.Printf(" --disable-features string comma-separated list of features to disable\n")
fmt.Printf(" --disable-all-features disable all features that can be disabled\n")
fmt.Printf(" --password string password to use for the instance\n")
fmt.Printf(" --disable-password disable password protection\n")
fmt.Printf(" -h show this help message\n")
}

View File

@@ -1367,6 +1367,37 @@ declare namespace $app {
mediaId?: number;
}
/**
* @event PreDeleteEntryEvent
* @file internal/platforms/platform/hook_events.go
* @description
* PreDeleteEntryEvent is triggered when an entry is about to be deleted.
* Prevent default to skip the default deletion and override the deletion.
*/
function onPreDeleteEntry(cb: (event: PreDeleteEntryEvent) => void): void;
interface PreDeleteEntryEvent {
mediaId?: number;
entryId?: number;
next(): void;
preventDefault(): void;
}
/**
* @event PostDeleteEntryEvent
* @file internal/platforms/platform/hook_events.go
*/
function onPostDeleteEntry(cb: (event: PostDeleteEntryEvent) => void): void;
interface PostDeleteEntryEvent {
mediaId?: number;
entryId?: number;
next(): void;
}
/**
* @package playbackmanager

View File

@@ -1085,6 +1085,12 @@ declare namespace $ui {
* @throws Error if a needed repository is not found
*/
getAnimeEntry(mediaId: number): Promise<$app.Anime_Entry>
/**
* Clears episode metadata cache.
* Note: To clear the anime entry cache, use $anilist.clearCache() (requires 'anilist' permission).
*/
clearEpisodeMetadataCache(): void
}
interface Manga {
@@ -1380,6 +1386,11 @@ declare namespace $storage {
}
declare namespace $anilist {
/**
* Deletes all cached data.
*/
function clearCache(): void
/**
* Refresh the anime collection.
* This will cause the frontend to refetch queries that depend on the anime collection.

View File

@@ -10,26 +10,3 @@
- Called before creation of a struct
- Native job cannot be interrupted even if `e.next()` isn't called
- Followed by event containing the struct, e.g. `onAnimeEntry`
### TODO
- [ ] Scanning
- [ ] Torrent client
- [ ] Torrent search
- [ ] AutoDownloader
- [ ] Torrent streaming
- [ ] Debrid / Debrid streaming
- [ ] PlaybackManager
- [ ] Media Player
- [ ] Sync / Offline
- [ ] Online streaming
- [ ] Metadata provider
- [ ] Manga
- [ ] Media streaming
---
- [ ] Command palette
- [ ] Database
- [ ] App Context

65
internal/hook/helper.go Normal file
View File

@@ -0,0 +1,65 @@
package hook
import "seanime/internal/hook_resolver"
type HookTriggerOptions[T hook_resolver.Resolver] struct {
Event T
Hook func() *Hook[hook_resolver.Resolver]
OnError func(error) error
OnDefaultPrevented func() error
OnSuccess func() error
}
// TriggerHook triggers the given hook with the provided event and handles
//
// Example:
// ok, err := hook.TriggerHook(&hook.HookTriggerOptions[*MissingEpisodesRequestedEvent]{
// Hook: hook.GlobalHookManager.OnMissingEpisodesRequested,
// Event: reqEvent,
// OnDefaultPrevented: func() error {
// event := new(MissingEpisodesEvent)
// event.MissingEpisodes = missing
// err = hook.GlobalHookManager.OnMissingEpisodes().Trigger(event)
// if err != nil {
// return nil
// }
// missing = event.MissingEpisodes
// return nil
// },
// OnSuccess: func() error {
// opts.AnimeCollection = reqEvent.AnimeCollection // Override the anime collection
// opts.LocalFiles = reqEvent.LocalFiles // Override the local files
// opts.SilencedMediaIds = reqEvent.SilencedMediaIds // Override the silenced media IDs
// missing = reqEvent.MissingEpisodes
// return nil
// },
// })
// if err != nil {
// return nil
// }
// if !ok {
// return missing
// }
func TriggerHook[T hook_resolver.Resolver](opts *HookTriggerOptions[T]) (cont bool, _ error) {
if err := opts.Hook().Trigger(opts.Event); err != nil {
// The hook errored out
if opts.OnError != nil {
return false, opts.OnError(err)
}
return false, err
}
// Default prevented
if opts.Event.IsDefaultPrevented() {
if opts.OnDefaultPrevented != nil {
return false, opts.OnDefaultPrevented()
}
// No error but don't continue
return false, nil
}
if opts.OnSuccess != nil {
return true, opts.OnSuccess()
}
return true, nil
}

View File

@@ -29,6 +29,8 @@ type Manager interface {
OnPostUpdateEntryProgress() *Hook[hook_resolver.Resolver]
OnPreUpdateEntryRepeat() *Hook[hook_resolver.Resolver]
OnPostUpdateEntryRepeat() *Hook[hook_resolver.Resolver]
OnPreDeleteEntry() *Hook[hook_resolver.Resolver]
OnPostDeleteEntry() *Hook[hook_resolver.Resolver]
// Anime library events
OnAnimeEntryRequested() *Hook[hook_resolver.Resolver]
@@ -171,6 +173,8 @@ type ManagerImpl struct {
onPostUpdateEntryProgress *Hook[hook_resolver.Resolver]
onPreUpdateEntryRepeat *Hook[hook_resolver.Resolver]
onPostUpdateEntryRepeat *Hook[hook_resolver.Resolver]
onPreDeleteEntry *Hook[hook_resolver.Resolver]
onPostDeleteEntry *Hook[hook_resolver.Resolver]
// Anime library events
onAnimeEntryRequested *Hook[hook_resolver.Resolver]
onAnimeEntry *Hook[hook_resolver.Resolver]
@@ -311,6 +315,8 @@ func (m *ManagerImpl) initHooks() {
m.onPostUpdateEntryProgress = &Hook[hook_resolver.Resolver]{}
m.onPreUpdateEntryRepeat = &Hook[hook_resolver.Resolver]{}
m.onPostUpdateEntryRepeat = &Hook[hook_resolver.Resolver]{}
m.onPreDeleteEntry = &Hook[hook_resolver.Resolver]{}
m.onPostDeleteEntry = &Hook[hook_resolver.Resolver]{}
// Anime library events
m.onAnimeEntryRequested = &Hook[hook_resolver.Resolver]{}
m.onAnimeEntry = &Hook[hook_resolver.Resolver]{}
@@ -541,6 +547,20 @@ func (m *ManagerImpl) OnPostUpdateEntryRepeat() *Hook[hook_resolver.Resolver] {
return m.onPostUpdateEntryRepeat
}
func (m *ManagerImpl) OnPreDeleteEntry() *Hook[hook_resolver.Resolver] {
if m == nil {
return &Hook[hook_resolver.Resolver]{}
}
return m.onPreDeleteEntry
}
func (m *ManagerImpl) OnPostDeleteEntry() *Hook[hook_resolver.Resolver] {
if m == nil {
return &Hook[hook_resolver.Resolver]{}
}
return m.onPostDeleteEntry
}
// Anime entry events
func (m *ManagerImpl) OnAnimeEntryRequested() *Hook[hook_resolver.Resolver] {

View File

@@ -10,6 +10,8 @@ type Resolver interface {
// PreventDefault prevents the native handler from being called.
PreventDefault()
IsDefaultPrevented() bool
SetNextFunc(f func() error)
}
@@ -44,6 +46,10 @@ func (e *Event) PreventDefault() {
e.DefaultPrevented = true
}
func (e *Event) IsDefaultPrevented() bool {
return e.DefaultPrevented
}
// NextFunc returns the function that Next calls.
func (e *Event) NextFunc() func() error {
return e.next

View File

@@ -21,6 +21,11 @@ import (
var episodeCollectionCache = result.NewBoundedCache[int, *EpisodeCollection](10)
var EpisodeCollectionFromLocalFilesCache = result.NewBoundedCache[int, *EpisodeCollection](10)
func ClearEpisodeCollectionCache() {
episodeCollectionCache.Clear()
EpisodeCollectionFromLocalFilesCache.Clear()
}
type (
// EpisodeCollection represents a collection of episodes.
EpisodeCollection struct {
@@ -187,10 +192,6 @@ func NewEpisodeCollection(opts NewEpisodeCollectionOptions) (ec *EpisodeCollecti
return
}
func ClearEpisodeCollectionCache() {
episodeCollectionCache.Clear()
}
/////////
type NewEpisodeCollectionFromLocalFilesOptions struct {

View File

@@ -40,6 +40,10 @@ func (mp *OfflineMetadataProvider) Close() {
// no-op
}
func (mp *OfflineMetadataProvider) ClearCache() {
// no-op
}
func (mp *OfflineMetadataProvider) SetUseFallbackProvider(useFallback bool) {
// no-op
}

36
internal/nakama/TODO.md Normal file
View File

@@ -0,0 +1,36 @@
# TODO
- Make a generic interface that combines PlaybackManager, DirectstreamManager/NativePlayer, OnlinestreamPlayer
- Generic events & calls so watch party manager doesn't need to know implementation details
- Generic player state
```go
package main
func main() {
wpm.playback.listenToPlayerEvents()
wpm.playback.StartLocalFileStream(...)
wpm.playback.StartTorrentStream(...)
wpm.playback.StartOnlineStream(...)
wpm.playback.StartDebridStream(...)
type PlaybackStatus struct {
ID string `json:"id"` // path or url
CompletionPercentage float64 `json:"completionPercentage"`
Playing bool `json:"playing"`
CurrentTime float64 `json:"currentTimeInSeconds"` // in seconds
Duration float64 `json:"durationInSeconds"` // in seconds
PlaybackType PlaybackType `json:"playbackType"` // file, torrentstream, onlinestream, debridstream
}
type PlaybackState struct {
EpisodeNumber int `json:"episodeNumber"` // The episode number
AniDbEpisode string `json:"aniDbEpisode"` // The AniDB episode number
MediaTitle string `json:"mediaTitle"` // The title of the media
MediaCoverImage string `json:"mediaCoverImage"` // The cover image of the media
MediaTotalEpisodes int `json:"mediaTotalEpisodes"` // The total number of episodes
MediaId int `json:"mediaId"` // The media ID
}
}
```

View File

@@ -159,16 +159,17 @@ func (ap *AnilistPlatform) UpdateEntryRepeat(ctx context.Context, mediaID int, r
func (ap *AnilistPlatform) DeleteEntry(ctx context.Context, mediaID, entryId int) error {
ap.logger.Trace().Msg("anilist platform: Deleting entry")
// Check if this is a custom source entry
if handled, err := ap.helper.HandleCustomSourceDeleteEntry(ctx, mediaID, entryId); handled {
return err
}
return ap.helper.TriggerDeleteEntryHooks(ctx, mediaID, entryId, func(event *platform.PreDeleteEntryEvent) error {
if handled, err := ap.helper.HandleCustomSourceDeleteEntry(ctx, *event.MediaID, *event.EntryID); handled {
return err
}
_, err := ap.anilistClient.DeleteEntry(ctx, &entryId)
if err != nil {
return err
}
return nil
_, err := ap.anilistClient.DeleteEntry(ctx, event.EntryID)
if err != nil {
return err
}
return nil
})
}
func (ap *AnilistPlatform) GetAnime(ctx context.Context, mediaID int) (*anilist.BaseAnime, error) {

View File

@@ -119,3 +119,17 @@ type PostUpdateEntryRepeatEvent struct {
hook_resolver.Event
MediaID *int `json:"mediaId"`
}
// PreDeleteEntryEvent is triggered when an entry is about to be deleted.
// Prevent default to skip the default deletion and override the deletion.
type PreDeleteEntryEvent struct {
hook_resolver.Event
MediaID *int `json:"mediaId"`
EntryID *int `json:"entryId"`
}
type PostDeleteEntryEvent struct {
hook_resolver.Event
MediaID *int `json:"mediaId"`
EntryID *int `json:"entryId"`
}

View File

@@ -478,6 +478,35 @@ func (h *PlatformHelper) TriggerUpdateEntryRepeatHooks(ctx context.Context, medi
return err
}
func (h *PlatformHelper) TriggerDeleteEntryHooks(ctx context.Context, mediaID int, entryId int, deleteFunc func(event *platform.PreDeleteEntryEvent) error) error {
// Trigger pre-delete hook
event := new(platform.PreDeleteEntryEvent)
event.MediaID = &mediaID
event.EntryID = &entryId
err := hook.GlobalHookManager.OnPreDeleteEntry().Trigger(event)
if err != nil {
return err
}
if event.DefaultPrevented {
return nil
}
// Execute the deletion
err = deleteFunc(event)
if err != nil {
return err
}
// Trigger post-delete hook
postEvent := new(platform.PostDeleteEntryEvent)
postEvent.MediaID = &mediaID
postEvent.EntryID = &entryId
err = hook.GlobalHookManager.OnPostDeleteEntry().Trigger(postEvent)
return err
}
func (h *PlatformHelper) FilterOutCustomAnimeLists(lists []*anilist.AnimeCollection_MediaListCollection_Lists) []*anilist.AnimeCollection_MediaListCollection_Lists {
return lo.Filter(lists, func(list *anilist.AnimeCollection_MediaListCollection_Lists, _ int) bool {
return list.Status != nil

View File

@@ -232,27 +232,28 @@ func (sp *SimulatedPlatform) UpdateEntryRepeat(ctx context.Context, mediaID int,
func (sp *SimulatedPlatform) DeleteEntry(ctx context.Context, mediaId, entryId int) error {
sp.logger.Trace().Int("entryId", entryId).Int("mediaId", mediaId).Msg("simulated platform: Deleting entry")
// Check if this is a custom source entry
if handled, err := sp.helper.HandleCustomSourceDeleteEntry(ctx, mediaId, entryId); handled {
return err
}
return sp.helper.TriggerDeleteEntryHooks(ctx, mediaId, entryId, func(event *platform.PreDeleteEntryEvent) error {
if handled, err := sp.helper.HandleCustomSourceDeleteEntry(ctx, *event.MediaID, *event.EntryID); handled {
return err
}
sp.mu.Lock()
defer sp.mu.Unlock()
sp.mu.Lock()
defer sp.mu.Unlock()
// Try anime first
wrapper := sp.GetAnimeCollectionWrapper()
if _, err := wrapper.FindEntry(entryId, true); err == nil {
return wrapper.DeleteEntry(entryId, true)
}
// Try anime first
wrapper := sp.GetAnimeCollectionWrapper()
if _, err := wrapper.FindEntry(*event.EntryID, true); err == nil {
return wrapper.DeleteEntry(*event.EntryID, true)
}
// Try manga
wrapper = sp.GetMangaCollectionWrapper()
if _, err := wrapper.FindEntry(entryId, true); err == nil {
return wrapper.DeleteEntry(entryId, true)
}
// Try manga
wrapper = sp.GetMangaCollectionWrapper()
if _, err := wrapper.FindEntry(*event.EntryID, true); err == nil {
return wrapper.DeleteEntry(*event.EntryID, true)
}
return ErrMediaNotFound
return ErrMediaNotFound
})
}
func (sp *SimulatedPlatform) GetAnime(ctx context.Context, mediaID int) (*anilist.BaseAnime, error) {

View File

@@ -5,6 +5,7 @@ import (
"seanime/internal/api/anilist"
"seanime/internal/events"
"seanime/internal/extension"
"seanime/internal/library/anime"
"github.com/dop251/goja"
"github.com/rs/zerolog"
@@ -78,7 +79,6 @@ func (a *AppContextImpl) BindAnilist(vm *goja.Runtime, logger *zerolog.Logger, e
_ = anilistObj.Set("getStudioDetails", func(studioID int) (*anilist.StudioDetails, error) {
return anilistPlatformRef.Get().GetStudioDetails(context.Background(), studioID)
})
_ = anilistObj.Set("listAnime", func(page *int, search *string, perPage *int, sort []*anilist.MediaSort, status []*anilist.MediaStatus, genres []*string, averageScoreGreater *int, season *anilist.MediaSeason, seasonYear *int, format *anilist.MediaFormat, isAdult *bool) (*anilist.ListAnime, error) {
return anilistPlatformRef.Get().GetAnilistClient().ListAnime(context.Background(), page, search, perPage, sort, status, genres, averageScoreGreater, season, seasonYear, format, isAdult)
})
@@ -88,6 +88,10 @@ func (a *AppContextImpl) BindAnilist(vm *goja.Runtime, logger *zerolog.Logger, e
_ = anilistObj.Set("listRecentAnime", func(page *int, perPage *int, airingAtGreater *int, airingAtLesser *int, notYetAired *bool) (*anilist.ListRecentAnime, error) {
return anilistPlatformRef.Get().GetAnilistClient().ListRecentAnime(context.Background(), page, perPage, airingAtGreater, airingAtLesser, notYetAired)
})
_ = anilistObj.Set("clearCache", func() {
anilistPlatformRef.Get().ClearCache()
anime.ClearEpisodeCollectionCache()
})
_ = anilistObj.Set("customQuery", func(body map[string]interface{}, token string) (interface{}, error) {
return anilist.CustomQuery(body, a.logger, token)
})

View File

@@ -34,6 +34,13 @@ func (a *AppContextImpl) BindAnimeToContextObj(vm *goja.Runtime, obj *goja.Objec
// Get downloaded chapter containers
_ = animeObj.Set("getAnimeEntry", m.getAnimeEntry)
_ = animeObj.Set("clearEpisodeMetadataCache", func(call goja.FunctionCall) goja.Value {
metadataProviderRef, ok := a.metadataProviderRef.Get()
if ok {
metadataProviderRef.Get().ClearCache()
}
return goja.Undefined()
})
_ = obj.Set("anime", animeObj)
}

BIN
seanime-web/public/jassub/jassub-worker-modern.wasm Normal file → Executable file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
seanime-web/public/jassub/jassub-worker.wasm Normal file → Executable file

Binary file not shown.

View File

@@ -32,6 +32,7 @@ export class VideoCorePgsRenderer {
private _isDestroyed: boolean = false
private _canvasWidth: number = 0
private _canvasHeight: number = 0
private _resizeObserver: ResizeObserver | null = null
constructor(options: VideoCorePgsRendererOptions) {
this._videoElement = options.videoElement
@@ -134,9 +135,9 @@ export class VideoCorePgsRenderer {
}
// Clean up resize observer
if (this._canvas && (this._canvas as any)._resizeObserver) {
(this._canvas as any)._resizeObserver.disconnect()
(this._canvas as any)._resizeObserver = null
if (this._resizeObserver) {
this._resizeObserver.disconnect()
this._resizeObserver = null
}
if (this._canvas && this._canvas.parentElement) {
@@ -213,14 +214,11 @@ export class VideoCorePgsRenderer {
private _setupResizeObserver() {
if (!this._videoElement || !this._canvas) return
const resizeObserver = new ResizeObserver(() => {
this._resizeObserver = new ResizeObserver(() => {
this.resize()
})
resizeObserver.observe(this._videoElement)
// Store for cleanup
;(this._canvas as any)._resizeObserver = resizeObserver
this._resizeObserver.observe(this._videoElement)
}
private _startRenderLoop() {

View File

@@ -398,7 +398,7 @@ Style: Default, Roboto Medium,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0
}
}
if (!this.pgsRenderer) {
if (!this.pgsRenderer && this.playbackInfo.mkvMetadata?.tracks?.some(t => isPGS(t.codecID))) {
this.pgsRenderer = new VideoCorePgsRenderer({
videoElement: this.videoElement,
// debug: process.env.NODE_ENV === "development",