node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,35 @@
### `Request` events
- Route scoped
- A handler that does the native job is called last and can be interrupted if `e.next()` isn't called
### `Requested` events
- Example: `onAnimeEntryRequested`
- 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

View File

@@ -0,0 +1,200 @@
package hook
import (
"seanime/internal/hook_resolver"
"sort"
"sync"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
// Handler defines a single Hook handler.
// Multiple handlers can share the same id.
// If Id is not explicitly set it will be autogenerated by Hook.Add and Hook.AddHandler.
type Handler[T hook_resolver.Resolver] struct {
// Func defines the handler function to execute.
//
// Note that users need to call e.Next() in order to proceed with
// the execution of the hook chain.
Func func(T) error
// Id is the unique identifier of the handler.
//
// It could be used later to remove the handler from a hook via [Hook.Remove].
//
// If missing, an autogenerated value will be assigned when adding
// the handler to a hook.
Id string
// Priority allows changing the default exec priority of the handler within a hook.
//
// If 0, the handler will be executed in the same order it was registered.
Priority int
}
// Hook defines a generic concurrent safe structure for managing event hooks.
// When using custom event it must embed the base [hook.Event].
//
// Example:
//
// type CustomEvent struct {
// hook.Event
// SomeField int
// }
//
// h := Hook[*CustomEvent]{}
//
// h.BindFunc(func(e *CustomEvent) error {
// println(e.SomeField)
//
// return e.Next()
// })
//
// h.Trigger(&CustomEvent{ SomeField: 123 })
type Hook[T hook_resolver.Resolver] struct {
handlers []*Handler[T]
mu sync.RWMutex
}
// Bind registers the provided handler to the current hooks queue.
//
// If handler.Id is empty it is updated with autogenerated value.
//
// If a handler from the current hook list has Id matching handler.Id
// then the old handler is replaced with the new one.
func (h *Hook[T]) Bind(handler *Handler[T]) string {
if h == nil {
return ""
}
h.mu.Lock()
defer h.mu.Unlock()
var exists bool
if handler.Id == "" {
handler.Id = generateHookId()
// ensure that it doesn't exist
DuplicateCheck:
for _, existing := range h.handlers {
if existing.Id == handler.Id {
handler.Id = generateHookId()
goto DuplicateCheck
}
}
} else {
// replace existing
for i, existing := range h.handlers {
if existing.Id == handler.Id {
h.handlers[i] = handler
exists = true
break
}
}
}
// append new
if !exists {
h.handlers = append(h.handlers, handler)
}
// sort handlers by Priority, preserving the original order of equal items
sort.SliceStable(h.handlers, func(i, j int) bool {
return h.handlers[i].Priority < h.handlers[j].Priority
})
return handler.Id
}
// BindFunc is similar to Bind but registers a new handler from just the provided function.
//
// The registered handler is added with a default 0 priority and the id will be autogenerated.
//
// If you want to register a handler with custom priority or id use the [Hook.Bind] method.
func (h *Hook[T]) BindFunc(fn func(e T) error) string {
return h.Bind(&Handler[T]{Func: fn})
}
// Unbind removes one or many hook handler by their id.
func (h *Hook[T]) Unbind(idsToRemove ...string) {
if h == nil {
return
}
h.mu.Lock()
defer h.mu.Unlock()
for _, id := range idsToRemove {
for i := len(h.handlers) - 1; i >= 0; i-- {
if h.handlers[i].Id == id {
h.handlers = append(h.handlers[:i], h.handlers[i+1:]...)
break // for now stop on the first occurrence since we don't allow handlers with duplicated ids
}
}
}
}
// UnbindAll removes all registered handlers.
func (h *Hook[T]) UnbindAll() {
if h == nil {
return
}
h.mu.Lock()
defer h.mu.Unlock()
h.handlers = nil
}
// Length returns to total number of registered hook handlers.
func (h *Hook[T]) Length() int {
if h == nil {
return 0
}
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.handlers)
}
// Trigger executes all registered hook handlers one by one
// with the specified event as an argument.
//
// Optionally, this method allows also to register additional one off
// handler funcs that will be temporary appended to the handlers queue.
//
// NB! Each hook handler must call event.Next() in order the hook chain to proceed.
func (h *Hook[T]) Trigger(event T, oneOffHandlerFuncs ...func(T) error) error {
if h == nil {
event.SetNextFunc(nil)
return event.Next()
}
h.mu.RLock()
handlers := make([]func(T) error, 0, len(h.handlers)+len(oneOffHandlerFuncs))
for _, handler := range h.handlers {
handlers = append(handlers, handler.Func)
}
handlers = append(handlers, oneOffHandlerFuncs...)
h.mu.RUnlock()
event.SetNextFunc(nil) // reset in case the event is being reused
for i := len(handlers) - 1; i >= 0; i-- {
i := i
old := event.NextFunc()
event.SetNextFunc(func() error {
event.SetNextFunc(old)
handlerErr := handlers[i](event)
if handlerErr != nil {
log.Error().Err(handlerErr).Msg("hook: Error in handler")
return handlerErr
}
return nil
})
}
return event.Next()
}
func generateHookId() string {
return uuid.New().String()
}

View File

@@ -0,0 +1,52 @@
package hook
import (
"errors"
"seanime/internal/hook_resolver"
"testing"
)
func TestHookAddHandlerAndAdd(t *testing.T) {
calls := ""
h := Hook[*hook_resolver.Event]{}
h.BindFunc(func(e *hook_resolver.Event) error { calls += "1"; return e.Next() })
h.BindFunc(func(e *hook_resolver.Event) error { calls += "2"; return e.Next() })
h3Id := h.BindFunc(func(e *hook_resolver.Event) error { calls += "3"; return e.Next() })
h.Bind(&Handler[*hook_resolver.Event]{
Id: h3Id, // should replace 3
Func: func(e *hook_resolver.Event) error { calls += "3'"; return e.Next() },
})
h.Bind(&Handler[*hook_resolver.Event]{
Func: func(e *hook_resolver.Event) error { calls += "4"; return e.Next() },
Priority: -2,
})
h.Bind(&Handler[*hook_resolver.Event]{
Func: func(e *hook_resolver.Event) error { calls += "5"; return e.Next() },
Priority: -1,
})
h.Bind(&Handler[*hook_resolver.Event]{
Func: func(e *hook_resolver.Event) error { calls += "6"; return e.Next() },
})
h.Bind(&Handler[*hook_resolver.Event]{
Func: func(e *hook_resolver.Event) error { calls += "7"; e.Next(); return errors.New("test") }, // error shouldn't stop the chain
})
h.Trigger(
&hook_resolver.Event{},
func(e *hook_resolver.Event) error { calls += "8"; return e.Next() },
func(e *hook_resolver.Event) error { calls += "9"; return nil }, // skip next
func(e *hook_resolver.Event) error { calls += "10"; return e.Next() },
)
if total := len(h.handlers); total != 7 {
t.Fatalf("Expected %d handlers, found %d", 7, total)
}
expectedCalls := "45123'6789"
if calls != expectedCalls {
t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
package hook
//
//// Tagger defines an interface for event data structs that support tags/groups/categories/etc.
//// Usually used together with TaggedHook.
//type Tagger interface {
// hook_events.Resolver
//
// Tags() []string
//}
//
//// wrapped local Hook embedded struct to limit the public API surface.
//type mainHook[T Tagger] struct {
// *Hook[T]
//}
//
//// NewTaggedHook creates a new TaggedHook with the provided main hook and optional tags.
//func NewTaggedHook[T Tagger](hook *Hook[T], tags ...string) *TaggedHook[T] {
// return &TaggedHook[T]{
// mainHook[T]{hook},
// tags,
// }
//}
//
//// TaggedHook defines a proxy hook which register handlers that are triggered only
//// if the TaggedHook.tags are empty or includes at least one of the event data tag(s).
//type TaggedHook[T Tagger] struct {
// mainHook[T]
//
// tags []string
//}
//
//// CanTriggerOn checks if the current TaggedHook can be triggered with
//// the provided event data tags.
////
//// It returns always true if the hook doens't have any tags.
//func (h *TaggedHook[T]) CanTriggerOn(tagsToCheck []string) bool {
// if len(h.tags) == 0 {
// return true // match all
// }
//
// for _, t := range tagsToCheck {
// if util.Contains(h.tags, t) {
// return true
// }
// }
//
// return false
//}
//
//// Bind registers the provided handler to the current hooks queue.
////
//// It is similar to [Hook.Bind] with the difference that the handler
//// function is invoked only if the event data tags satisfy h.CanTriggerOn.
//func (h *TaggedHook[T]) Bind(handler *Handler[T]) string {
// fn := handler.Func
//
// handler.Func = func(e T) error {
// if h.CanTriggerOn(e.Tags()) {
// return fn(e)
// }
//
// return e.Next()
// }
//
// return h.mainHook.Bind(handler)
//}
//
//// BindFunc registers a new handler with the specified function.
////
//// It is similar to [Hook.Bind] with the difference that the handler
//// function is invoked only if the event data tags satisfy h.CanTriggerOn.
//func (h *TaggedHook[T]) BindFunc(fn func(e T) error) string {
// return h.mainHook.BindFunc(func(e T) error {
// if h.CanTriggerOn(e.Tags()) {
// return fn(e)
// }
//
// return e.Next()
// })
//}

View File

@@ -0,0 +1,79 @@
package hook
//type mockTagsEvent struct {
// Event
// tags []string
//}
//
//func (m mockTagsEvent) Tags() []string {
// return m.tags
//}
//
//func TestTaggedHook(t *testing.T) {
// calls := ""
//
// base := &Hook[*mockTagsEvent]{}
// base.BindFunc(func(e *mockTagsEvent) error { calls += "f0"; return e.Next() })
//
// hA := NewTaggedHook(base)
// hA.BindFunc(func(e *mockTagsEvent) error { calls += "a1"; return e.Next() })
// hA.Bind(&Handler[*mockTagsEvent]{
// Func: func(e *mockTagsEvent) error { calls += "a2"; return e.Next() },
// Priority: -1,
// })
//
// hB := NewTaggedHook(base, "b1", "b2")
// hB.BindFunc(func(e *mockTagsEvent) error { calls += "b1"; return e.Next() })
// hB.Bind(&Handler[*mockTagsEvent]{
// Func: func(e *mockTagsEvent) error { calls += "b2"; return e.Next() },
// Priority: -2,
// })
//
// hC := NewTaggedHook(base, "c1", "c2")
// hC.BindFunc(func(e *mockTagsEvent) error { calls += "c1"; return e.Next() })
// hC.Bind(&Handler[*mockTagsEvent]{
// Func: func(e *mockTagsEvent) error { calls += "c2"; return e.Next() },
// Priority: -3,
// })
//
// scenarios := []struct {
// event *mockTagsEvent
// expectedCalls string
// }{
// {
// &mockTagsEvent{},
// "a2f0a1",
// },
// {
// &mockTagsEvent{tags: []string{"missing"}},
// "a2f0a1",
// },
// {
// &mockTagsEvent{tags: []string{"b2"}},
// "b2a2f0a1b1",
// },
// {
// &mockTagsEvent{tags: []string{"c1"}},
// "c2a2f0a1c1",
// },
// {
// &mockTagsEvent{tags: []string{"b1", "c2"}},
// "c2b2a2f0a1b1c1",
// },
// }
//
// for _, s := range scenarios {
// t.Run(strings.Join(s.event.tags, "_"), func(t *testing.T) {
// calls = "" // reset
//
// err := base.Trigger(s.event)
// if err != nil {
// t.Fatalf("Unexpected trigger error: %v", err)
// }
//
// if calls != s.expectedCalls {
// t.Fatalf("Expected calls sequence %q, got %q", s.expectedCalls, calls)
// }
// })
// }
//}