node build fixed
This commit is contained in:
122
seanime-2.9.10/internal/onlinestream/manual_mapping.go
Normal file
122
seanime-2.9.10/internal/onlinestream/manual_mapping.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package onlinestream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/result"
|
||||
"strings"
|
||||
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
var searchResultCache = result.NewCache[string, []*hibikeonlinestream.SearchResult]()
|
||||
|
||||
func (r *Repository) ManualSearch(provider string, query string, dub bool) (ret []*hibikeonlinestream.SearchResult, err error) {
|
||||
defer util.HandlePanicInModuleWithError("onlinestream/ManualSearch", &err)
|
||||
|
||||
if query == "" {
|
||||
return make([]*hibikeonlinestream.SearchResult, 0), nil
|
||||
}
|
||||
|
||||
// Get the search results
|
||||
providerExtension, ok := extension.GetExtension[extension.OnlinestreamProviderExtension](r.providerExtensionBank, provider)
|
||||
if !ok {
|
||||
r.logger.Error().Str("provider", provider).Msg("onlinestream: Provider not found")
|
||||
return nil, errors.New("onlinestream: Provider not found")
|
||||
}
|
||||
|
||||
normalizedQuery := strings.ToLower(strings.TrimSpace(query))
|
||||
|
||||
searchRes, found := searchResultCache.Get(provider + normalizedQuery + fmt.Sprintf("%t", dub))
|
||||
if found {
|
||||
return searchRes, nil
|
||||
}
|
||||
|
||||
searchRes, err = providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
Query: normalizedQuery,
|
||||
Dub: dub,
|
||||
Year: 0,
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Str("query", normalizedQuery).Msg("onlinestream: Search failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchResultCache.Set(provider+normalizedQuery+fmt.Sprintf("%t", dub), searchRes)
|
||||
|
||||
return searchRes, nil
|
||||
}
|
||||
|
||||
// ManualMapping is used to manually map an anime to a provider.
|
||||
// After calling this, the client should re-fetch the episode list.
|
||||
func (r *Repository) ManualMapping(provider string, mediaId int, animeId string) (err error) {
|
||||
defer util.HandlePanicInModuleWithError("onlinestream/ManualMapping", &err)
|
||||
|
||||
r.logger.Trace().Msgf("onlinestream: Removing cached bucket for %s, media ID: %d", provider, mediaId)
|
||||
|
||||
// Delete the cached data if any
|
||||
epListBucket := r.getFcEpisodeListBucket(provider, mediaId)
|
||||
_ = r.fileCacher.Remove(epListBucket.Name())
|
||||
epDataBucket := r.getFcEpisodeDataBucket(provider, mediaId)
|
||||
_ = r.fileCacher.Remove(epDataBucket.Name())
|
||||
|
||||
r.logger.Trace().
|
||||
Str("provider", provider).
|
||||
Int("mediaId", mediaId).
|
||||
Str("animeId", animeId).
|
||||
Msg("onlinestream: Manual mapping")
|
||||
|
||||
// Insert the mapping into the database
|
||||
err = r.db.InsertOnlinestreamMapping(provider, mediaId, animeId)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to insert mapping")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Msg("onlinestream: Manual mapping successful")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MappingResponse struct {
|
||||
AnimeId *string `json:"animeId"`
|
||||
}
|
||||
|
||||
func (r *Repository) GetMapping(provider string, mediaId int) (ret MappingResponse) {
|
||||
defer util.HandlePanicInModuleThen("onlinestream/GetMapping", func() {
|
||||
ret = MappingResponse{}
|
||||
})
|
||||
|
||||
mapping, found := r.db.GetOnlinestreamMapping(provider, mediaId)
|
||||
if !found {
|
||||
return MappingResponse{}
|
||||
}
|
||||
|
||||
return MappingResponse{
|
||||
AnimeId: &mapping.AnimeID,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) RemoveMapping(provider string, mediaId int) (err error) {
|
||||
defer util.HandlePanicInModuleWithError("onlinestream/RemoveMapping", &err)
|
||||
|
||||
// Delete the mapping from the database
|
||||
err = r.db.DeleteOnlinestreamMapping(provider, mediaId)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to delete mapping")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Msg("onlinestream: Mapping removed")
|
||||
|
||||
r.logger.Trace().Msgf("onlinestream: Removing cached bucket for %s, media ID: %d", provider, mediaId)
|
||||
// Delete the cached data if any
|
||||
epListBucket := r.getFcEpisodeListBucket(provider, mediaId)
|
||||
_ = r.fileCacher.Remove(epListBucket.Name())
|
||||
epDataBucket := r.getFcEpisodeDataBucket(provider, mediaId)
|
||||
_ = r.fileCacher.Remove(epDataBucket.Name())
|
||||
|
||||
return nil
|
||||
}
|
||||
446
seanime-2.9.10/internal/onlinestream/providers/_animepahe.go
Normal file
446
seanime-2.9.10/internal/onlinestream/providers/_animepahe.go
Normal file
@@ -0,0 +1,446 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gocolly/colly"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
onlinestream_sources "seanime/internal/onlinestream/sources"
|
||||
"seanime/internal/util"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
Animepahe struct {
|
||||
BaseURL string
|
||||
Client http.Client
|
||||
UserAgent string
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
AnimepaheSearchResult struct {
|
||||
Data []struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Year int `json:"year"`
|
||||
Poster string `json:"poster"`
|
||||
Type string `json:"type"`
|
||||
Session string `json:"session"`
|
||||
} `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewAnimepahe(logger *zerolog.Logger) hibikeonlinestream.Provider {
|
||||
return &Animepahe{
|
||||
BaseURL: "https://animepahe.ru",
|
||||
Client: http.Client{},
|
||||
UserAgent: util.GetRandomUserAgent(),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Animepahe) GetSettings() hibikeonlinestream.Settings {
|
||||
return hibikeonlinestream.Settings{
|
||||
EpisodeServers: []string{"animepahe"},
|
||||
SupportsDub: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Animepahe) Search(opts hibikeonlinestream.SearchOptions) ([]*hibikeonlinestream.SearchResult, error) {
|
||||
var results []*hibikeonlinestream.SearchResult
|
||||
|
||||
query := opts.Query
|
||||
dubbed := opts.Dub
|
||||
|
||||
g.logger.Debug().Str("query", query).Bool("dubbed", dubbed).Msg("animepahe: Searching anime")
|
||||
|
||||
q := url.QueryEscape(query)
|
||||
request, err := http.NewRequest("GET", g.BaseURL+fmt.Sprintf("/api?m=search&q=%s", q), nil)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to create request")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", g.UserAgent)
|
||||
request.Header.Set("Cookie", "__ddg1_=;__ddg2_=;")
|
||||
|
||||
response, err := g.Client.Do(request)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to send request")
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var searchResult AnimepaheSearchResult
|
||||
err = json.NewDecoder(response.Body).Decode(&searchResult)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to decode response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, data := range searchResult.Data {
|
||||
results = append(results, &hibikeonlinestream.SearchResult{
|
||||
ID: cmp.Or(fmt.Sprintf("%d", data.ID), data.Session),
|
||||
Title: data.Title,
|
||||
URL: fmt.Sprintf("%s/anime/%d", g.BaseURL, data.ID),
|
||||
SubOrDub: hibikeonlinestream.Sub,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (g *Animepahe) FindEpisodes(id string) ([]*hibikeonlinestream.EpisodeDetails, error) {
|
||||
var episodes []*hibikeonlinestream.EpisodeDetails
|
||||
|
||||
q1 := fmt.Sprintf("/anime/%s", id)
|
||||
if !strings.Contains(id, "-") {
|
||||
q1 = fmt.Sprintf("/a/%s", id)
|
||||
}
|
||||
c := colly.NewCollector(
|
||||
colly.UserAgent(g.UserAgent),
|
||||
)
|
||||
|
||||
c.OnRequest(func(r *colly.Request) {
|
||||
r.Headers.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
})
|
||||
|
||||
var tempId string
|
||||
c.OnHTML("head > meta[property='og:url']", func(e *colly.HTMLElement) {
|
||||
parts := strings.Split(e.Attr("content"), "/")
|
||||
tempId = parts[len(parts)-1]
|
||||
})
|
||||
|
||||
err := c.Visit(g.BaseURL + q1)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to fetch episodes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// { last_page: number; data: { id: number; episode: number; title: string; snapshot: string; filler: number; created_at?: string }[] }
|
||||
type data struct {
|
||||
LastPage int `json:"last_page"`
|
||||
Data []struct {
|
||||
ID int `json:"id"`
|
||||
Episode int `json:"episode"`
|
||||
Title string `json:"title"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
Filler int `json:"filler"`
|
||||
Session string `json:"session"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
q2 := fmt.Sprintf("/api?m=release&id=%s&sort=episode_asc&page=1", tempId)
|
||||
request, err := http.NewRequest("GET", g.BaseURL+q2, nil)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to create request")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", g.UserAgent)
|
||||
request.Header.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
|
||||
response, err := g.Client.Do(request)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to send request")
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var d data
|
||||
err = json.NewDecoder(response.Body).Decode(&d)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to decode response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range d.Data {
|
||||
episodes = append(episodes, &hibikeonlinestream.EpisodeDetails{
|
||||
Provider: "animepahe",
|
||||
ID: fmt.Sprintf("%d$%s", e.ID, id),
|
||||
Number: e.Episode,
|
||||
URL: fmt.Sprintf("%s/anime/%s/%d", g.BaseURL, id, e.Episode),
|
||||
Title: cmp.Or(e.Title, "Episode "+fmt.Sprintf("%d", e.Episode)),
|
||||
})
|
||||
}
|
||||
|
||||
var pageNumbers []int
|
||||
|
||||
for i := 2; i <= d.LastPage; i++ {
|
||||
pageNumbers = append(pageNumbers, i)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(pageNumbers))
|
||||
mu := sync.Mutex{}
|
||||
|
||||
for _, p := range pageNumbers {
|
||||
go func(p int) {
|
||||
defer wg.Done()
|
||||
q2 := fmt.Sprintf("/api?m=release&id=%s&sort=episode_asc&page=%d", tempId, p)
|
||||
request, err := http.NewRequest("GET", g.BaseURL+q2, nil)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to create request")
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", g.UserAgent)
|
||||
request.Header.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
|
||||
response, err := g.Client.Do(request)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to send request")
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var d data
|
||||
err = json.NewDecoder(response.Body).Decode(&d)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to decode response")
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
for _, e := range d.Data {
|
||||
episodes = append(episodes, &hibikeonlinestream.EpisodeDetails{
|
||||
Provider: "animepahe",
|
||||
ID: fmt.Sprintf("%d$%s", e.ID, id),
|
||||
Number: e.Episode,
|
||||
URL: fmt.Sprintf("%s/anime/%s/%d", g.BaseURL, id, e.Episode),
|
||||
Title: cmp.Or(e.Title, "Episode "+fmt.Sprintf("%d", e.Episode)),
|
||||
})
|
||||
}
|
||||
mu.Unlock()
|
||||
}(p)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
g.logger.Debug().Int("count", len(episodes)).Msg("animepahe: Fetched episodes")
|
||||
|
||||
sort.Slice(episodes, func(i, j int) bool {
|
||||
return episodes[i].Number < episodes[j].Number
|
||||
})
|
||||
|
||||
if len(episodes) == 0 {
|
||||
return nil, fmt.Errorf("no episodes found")
|
||||
}
|
||||
|
||||
// Normalize episode numbers
|
||||
offset := episodes[0].Number + 1
|
||||
for i, e := range episodes {
|
||||
episodes[i].Number = e.Number - offset
|
||||
}
|
||||
|
||||
return episodes, nil
|
||||
}
|
||||
|
||||
func (g *Animepahe) FindEpisodeServer(episodeInfo *hibikeonlinestream.EpisodeDetails, server string) (*hibikeonlinestream.EpisodeServer, error) {
|
||||
var source *hibikeonlinestream.EpisodeServer
|
||||
|
||||
parts := strings.Split(episodeInfo.ID, "$")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("animepahe: Invalid episode ID")
|
||||
}
|
||||
|
||||
episodeID := parts[0]
|
||||
animeID := parts[1]
|
||||
|
||||
q1 := fmt.Sprintf("/anime/%s", animeID)
|
||||
if !strings.Contains(animeID, "-") {
|
||||
q1 = fmt.Sprintf("/a/%s", animeID)
|
||||
}
|
||||
c := colly.NewCollector(
|
||||
colly.UserAgent(g.UserAgent),
|
||||
)
|
||||
|
||||
var reqUrl *url.URL
|
||||
|
||||
c.OnRequest(func(r *colly.Request) {
|
||||
r.Headers.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
})
|
||||
|
||||
c.OnResponse(func(r *colly.Response) {
|
||||
reqUrl = r.Request.URL
|
||||
})
|
||||
|
||||
var tempId string
|
||||
c.OnHTML("head > meta[property='og:url']", func(e *colly.HTMLElement) {
|
||||
parts := strings.Split(e.Attr("content"), "/")
|
||||
tempId = parts[len(parts)-1]
|
||||
})
|
||||
|
||||
err := c.Visit(g.BaseURL + q1)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to fetch episodes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sessionId string
|
||||
// retain url without query
|
||||
reqUrlStr := reqUrl.Path
|
||||
reqUrlStrParts := strings.Split(reqUrlStr, "/anime/")
|
||||
sessionId = reqUrlStrParts[len(reqUrlStrParts)-1]
|
||||
|
||||
// { last_page: number; data: { id: number; episode: number; title: string; snapshot: string; filler: number; created_at?: string }[] }
|
||||
type data struct {
|
||||
LastPage int `json:"last_page"`
|
||||
Data []struct {
|
||||
ID int `json:"id"`
|
||||
Session string `json:"session"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
q2 := fmt.Sprintf("/api?m=release&id=%s&sort=episode_asc&page=1", tempId)
|
||||
request, err := http.NewRequest("GET", g.BaseURL+q2, nil)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to create request")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", g.UserAgent)
|
||||
request.Header.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
|
||||
response, err := g.Client.Do(request)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to send request")
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var d data
|
||||
err = json.NewDecoder(response.Body).Decode(&d)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to decode response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
episodeSession := ""
|
||||
|
||||
for _, e := range d.Data {
|
||||
if fmt.Sprintf("%d", e.ID) == episodeID {
|
||||
episodeSession = e.Session
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var pageNumbers []int
|
||||
|
||||
for i := 1; i <= d.LastPage; i++ {
|
||||
pageNumbers = append(pageNumbers, i)
|
||||
}
|
||||
|
||||
if episodeSession == "" {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(pageNumbers))
|
||||
mu := sync.Mutex{}
|
||||
|
||||
for _, p := range pageNumbers {
|
||||
go func(p int) {
|
||||
defer wg.Done()
|
||||
q2 := fmt.Sprintf("/api?m=release&id=%s&sort=episode_asc&page=%d", tempId, p)
|
||||
request, err := http.NewRequest("GET", g.BaseURL+q2, nil)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to create request")
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", g.UserAgent)
|
||||
request.Header.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
|
||||
response, err := g.Client.Do(request)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to send request")
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var d data
|
||||
err = json.NewDecoder(response.Body).Decode(&d)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to decode response")
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
for _, e := range d.Data {
|
||||
if fmt.Sprintf("%d", e.ID) == episodeID {
|
||||
episodeSession = e.Session
|
||||
break
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}(p)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
if episodeSession == "" {
|
||||
return nil, fmt.Errorf("animepahe: Episode not found")
|
||||
}
|
||||
|
||||
q3 := fmt.Sprintf("/play/%s/%s", sessionId, episodeSession)
|
||||
request2, err := http.NewRequest("GET", g.BaseURL+q3, nil)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to create request")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request2.Header.Set("User-Agent", g.UserAgent)
|
||||
request2.Header.Set("Cookie", "__ddg1_=;__ddg2_=")
|
||||
|
||||
response2, err := g.Client.Do(request2)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to send request")
|
||||
return nil, err
|
||||
}
|
||||
defer response2.Body.Close()
|
||||
|
||||
htmlString := ""
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(response2.Body)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to parse response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
htmlString = doc.Text()
|
||||
|
||||
//const regex = /https:\/\/kwik\.si\/e\/\w+/g;
|
||||
// const matches = watchReq.match(regex);
|
||||
//
|
||||
// if (matches === null) return undefined;
|
||||
|
||||
re := regexp.MustCompile(`https:\/\/kwik\.si\/e\/\w+`)
|
||||
matches := re.FindAllString(htmlString, -1)
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("animepahe: Failed to find episode source")
|
||||
}
|
||||
|
||||
kwik := onlinestream_sources.NewKwik()
|
||||
videoSources, err := kwik.Extract(matches[0])
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("animepahe: Failed to extract video sources")
|
||||
return nil, fmt.Errorf("animepahe: Failed to extract video sources, %w", err)
|
||||
}
|
||||
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: "animepahe",
|
||||
Server: KwikServer,
|
||||
Headers: map[string]string{"Referer": "https://kwik.si/"},
|
||||
VideoSources: videoSources,
|
||||
}
|
||||
|
||||
return source, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnimepahe_Search(t *testing.T) {
|
||||
|
||||
ap := NewAnimepahe(util.NewLogger())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
dubbed bool
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
query: "One Piece",
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "Blue Lock Season 2",
|
||||
query: "Blue Lock Season 2",
|
||||
dubbed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
results, err := ap.Search(hibikeonlinestream.SearchOptions{
|
||||
Query: tt.query,
|
||||
Dub: tt.dubbed,
|
||||
})
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, results)
|
||||
|
||||
for _, r := range results {
|
||||
assert.NotEmpty(t, r.ID, "ID is empty")
|
||||
assert.NotEmpty(t, r.Title, "Title is empty")
|
||||
assert.NotEmpty(t, r.URL, "URL is empty")
|
||||
}
|
||||
|
||||
util.Spew(results)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAnimepahe_FetchEpisodes(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
id: "4",
|
||||
},
|
||||
{
|
||||
name: "Blue Lock Season 2",
|
||||
id: "5648",
|
||||
},
|
||||
}
|
||||
|
||||
ap := NewAnimepahe(util.NewLogger())
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
episodes, err := ap.FindEpisodes(tt.id)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, episodes)
|
||||
|
||||
for _, e := range episodes {
|
||||
assert.NotEmpty(t, e.ID, "ID is empty")
|
||||
assert.NotEmpty(t, e.Number, "Number is empty")
|
||||
assert.NotEmpty(t, e.URL, "URL is empty")
|
||||
}
|
||||
|
||||
util.Spew(episodes)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAnimepahe_FetchSources(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
episode *hibikeonlinestream.EpisodeDetails
|
||||
server string
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "63391$4",
|
||||
Number: 1115,
|
||||
URL: "",
|
||||
},
|
||||
server: KwikServer,
|
||||
},
|
||||
{
|
||||
name: "Blue Lock Season 2 - Episode 1",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "64056$5648",
|
||||
Number: 1,
|
||||
URL: "",
|
||||
},
|
||||
server: KwikServer,
|
||||
},
|
||||
}
|
||||
ap := NewAnimepahe(util.NewLogger())
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
sources, err := ap.FindEpisodeServer(tt.episode, tt.server)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrSourceNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Source not found")
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, sources)
|
||||
|
||||
for _, s := range sources.VideoSources {
|
||||
assert.NotEmpty(t, s, "Source is empty")
|
||||
}
|
||||
|
||||
util.Spew(sources)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
25
seanime-2.9.10/internal/onlinestream/providers/common.go
Normal file
25
seanime-2.9.10/internal/onlinestream/providers/common.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import "errors"
|
||||
|
||||
// Built-in
|
||||
const (
|
||||
GogoanimeProvider string = "gogoanime"
|
||||
ZoroProvider string = "zoro"
|
||||
)
|
||||
|
||||
// Built-in
|
||||
const (
|
||||
DefaultServer = "default"
|
||||
GogocdnServer = "gogocdn"
|
||||
VidstreamingServer = "vidstreaming"
|
||||
StreamSBServer = "streamsb"
|
||||
VidcloudServer = "vidcloud"
|
||||
StreamtapeServer = "streamtape"
|
||||
KwikServer = "kwik"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSourceNotFound = errors.New("video source not found")
|
||||
ErrServerNotFound = errors.New("server not found")
|
||||
)
|
||||
247
seanime-2.9.10/internal/onlinestream/providers/gogoanime.go
Normal file
247
seanime-2.9.10/internal/onlinestream/providers/gogoanime.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gocolly/colly"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/onlinestream/sources"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
type Gogoanime struct {
|
||||
BaseURL string
|
||||
AjaxURL string
|
||||
Client http.Client
|
||||
UserAgent string
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewGogoanime(logger *zerolog.Logger) hibikeonlinestream.Provider {
|
||||
return &Gogoanime{
|
||||
BaseURL: "https://anitaku.to",
|
||||
AjaxURL: "https://ajax.gogocdn.net",
|
||||
Client: http.Client{},
|
||||
UserAgent: util.GetRandomUserAgent(),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gogoanime) GetSettings() hibikeonlinestream.Settings {
|
||||
return hibikeonlinestream.Settings{
|
||||
EpisodeServers: []string{GogocdnServer, VidstreamingServer},
|
||||
SupportsDub: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gogoanime) Search(opts hibikeonlinestream.SearchOptions) ([]*hibikeonlinestream.SearchResult, error) {
|
||||
var results []*hibikeonlinestream.SearchResult
|
||||
|
||||
query := opts.Query
|
||||
dubbed := opts.Dub
|
||||
|
||||
g.logger.Debug().Str("query", query).Bool("dubbed", dubbed).Msg("gogoanime: Searching anime")
|
||||
|
||||
c := colly.NewCollector(
|
||||
colly.UserAgent(g.UserAgent),
|
||||
)
|
||||
|
||||
c.OnHTML(".last_episodes > ul > li", func(e *colly.HTMLElement) {
|
||||
id := ""
|
||||
idParts := strings.Split(e.ChildAttr("p.name > a", "href"), "/")
|
||||
if len(idParts) > 2 {
|
||||
id = idParts[2]
|
||||
}
|
||||
title := e.ChildText("p.name > a")
|
||||
url := g.BaseURL + e.ChildAttr("p.name > a", "href")
|
||||
subOrDub := hibikeonlinestream.Sub
|
||||
if strings.Contains(strings.ToLower(e.ChildText("p.name > a")), "dub") {
|
||||
subOrDub = hibikeonlinestream.Dub
|
||||
}
|
||||
results = append(results, &hibikeonlinestream.SearchResult{
|
||||
ID: id,
|
||||
Title: title,
|
||||
URL: url,
|
||||
SubOrDub: subOrDub,
|
||||
})
|
||||
})
|
||||
|
||||
searchURL := g.BaseURL + "/search.html?keyword=" + url.QueryEscape(query)
|
||||
if dubbed {
|
||||
searchURL += "%20(Dub)"
|
||||
}
|
||||
|
||||
err := c.Visit(searchURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.logger.Debug().Int("count", len(results)).Msg("gogoanime: Fetched anime")
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (g *Gogoanime) FindEpisodes(id string) ([]*hibikeonlinestream.EpisodeDetails, error) {
|
||||
var episodes []*hibikeonlinestream.EpisodeDetails
|
||||
|
||||
g.logger.Debug().Str("id", id).Msg("gogoanime: Fetching episodes")
|
||||
|
||||
if !strings.Contains(id, "gogoanime") {
|
||||
id = fmt.Sprintf("%s/category/%s", g.BaseURL, id)
|
||||
}
|
||||
|
||||
c := colly.NewCollector(
|
||||
colly.UserAgent(g.UserAgent),
|
||||
)
|
||||
|
||||
var epStart, epEnd, movieID, alias string
|
||||
|
||||
c.OnHTML("#episode_page > li > a", func(e *colly.HTMLElement) {
|
||||
if epStart == "" {
|
||||
epStart = e.Attr("ep_start")
|
||||
}
|
||||
epEnd = e.Attr("ep_end")
|
||||
})
|
||||
|
||||
c.OnHTML("#movie_id", func(e *colly.HTMLElement) {
|
||||
movieID = e.Attr("value")
|
||||
})
|
||||
|
||||
c.OnHTML("#alias", func(e *colly.HTMLElement) {
|
||||
alias = e.Attr("value")
|
||||
})
|
||||
|
||||
err := c.Visit(id)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("gogoanime: Failed to fetch episodes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c2 := colly.NewCollector(
|
||||
colly.UserAgent(g.UserAgent),
|
||||
)
|
||||
|
||||
c2.OnHTML("#episode_related > li", func(e *colly.HTMLElement) {
|
||||
episodeIDParts := strings.Split(e.ChildAttr("a", "href"), "/")
|
||||
if len(episodeIDParts) < 2 {
|
||||
return
|
||||
}
|
||||
episodeID := strings.TrimSpace(episodeIDParts[1])
|
||||
episodeNumberStr := strings.TrimPrefix(e.ChildText("div.name"), "EP ")
|
||||
episodeNumber, err := strconv.Atoi(episodeNumberStr)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Str("episodeID", episodeID).Msg("failed to parse episode number")
|
||||
return
|
||||
}
|
||||
episodes = append(episodes, &hibikeonlinestream.EpisodeDetails{
|
||||
Provider: GogoanimeProvider,
|
||||
ID: episodeID,
|
||||
Number: episodeNumber,
|
||||
URL: g.BaseURL + "/" + episodeID,
|
||||
})
|
||||
})
|
||||
|
||||
ajaxURL := fmt.Sprintf("%s/ajax/load-list-episode", g.AjaxURL)
|
||||
ajaxParams := url.Values{
|
||||
"ep_start": {epStart},
|
||||
"ep_end": {epEnd},
|
||||
"id": {movieID},
|
||||
"alias": {alias},
|
||||
"default_ep": {"0"},
|
||||
}
|
||||
ajaxURLWithParams := fmt.Sprintf("%s?%s", ajaxURL, ajaxParams.Encode())
|
||||
|
||||
err = c2.Visit(ajaxURLWithParams)
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("gogoanime: Failed to fetch episodes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.logger.Debug().Int("count", len(episodes)).Msg("gogoanime: Fetched episodes")
|
||||
|
||||
return episodes, nil
|
||||
}
|
||||
|
||||
func (g *Gogoanime) FindEpisodeServer(episodeInfo *hibikeonlinestream.EpisodeDetails, server string) (*hibikeonlinestream.EpisodeServer, error) {
|
||||
var source *hibikeonlinestream.EpisodeServer
|
||||
|
||||
if server == DefaultServer {
|
||||
server = GogocdnServer
|
||||
}
|
||||
g.logger.Debug().Str("server", string(server)).Str("episodeID", episodeInfo.ID).Msg("gogoanime: Fetching server sources")
|
||||
|
||||
c := colly.NewCollector()
|
||||
|
||||
switch server {
|
||||
case VidstreamingServer:
|
||||
c.OnHTML(".anime_muti_link > ul > li.vidcdn > a", func(e *colly.HTMLElement) {
|
||||
src := e.Attr("data-video")
|
||||
gogocdn := onlinestream_sources.NewGogoCDN()
|
||||
videoSources, err := gogocdn.Extract(src)
|
||||
if err == nil {
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: GogoanimeProvider,
|
||||
Server: server,
|
||||
Headers: map[string]string{
|
||||
"Referer": g.BaseURL + "/" + episodeInfo.ID,
|
||||
},
|
||||
VideoSources: videoSources,
|
||||
}
|
||||
}
|
||||
})
|
||||
case GogocdnServer, "":
|
||||
c.OnHTML("#load_anime > div > div > iframe", func(e *colly.HTMLElement) {
|
||||
src := e.Attr("src")
|
||||
gogocdn := onlinestream_sources.NewGogoCDN()
|
||||
videoSources, err := gogocdn.Extract(src)
|
||||
if err == nil {
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: GogoanimeProvider,
|
||||
Server: server,
|
||||
Headers: map[string]string{
|
||||
"Referer": g.BaseURL + "/" + episodeInfo.ID,
|
||||
},
|
||||
VideoSources: videoSources,
|
||||
}
|
||||
}
|
||||
})
|
||||
case StreamSBServer:
|
||||
c.OnHTML(".anime_muti_link > ul > li.streamsb > a", func(e *colly.HTMLElement) {
|
||||
src := e.Attr("data-video")
|
||||
streamsb := onlinestream_sources.NewStreamSB()
|
||||
videoSources, err := streamsb.Extract(src)
|
||||
if err == nil {
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: GogoanimeProvider,
|
||||
Server: server,
|
||||
Headers: map[string]string{
|
||||
"Referer": g.BaseURL + "/" + episodeInfo.ID,
|
||||
"watchsb": "streamsb",
|
||||
"User-Agent": g.UserAgent,
|
||||
},
|
||||
VideoSources: videoSources,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err := c.Visit(g.BaseURL + "/" + episodeInfo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if source == nil {
|
||||
g.logger.Warn().Str("server", server).Msg("gogoanime: No sources found")
|
||||
return nil, ErrSourceNotFound
|
||||
}
|
||||
|
||||
g.logger.Debug().Str("server", server).Int("videoSources", len(source.VideoSources)).Msg("gogoanime: Fetched server sources")
|
||||
|
||||
return source, nil
|
||||
|
||||
}
|
||||
172
seanime-2.9.10/internal/onlinestream/providers/gogoanime_test.go
Normal file
172
seanime-2.9.10/internal/onlinestream/providers/gogoanime_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGogoanime_Search(t *testing.T) {
|
||||
|
||||
gogo := NewGogoanime(util.NewLogger())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
dubbed bool
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
query: "One Piece",
|
||||
dubbed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
results, err := gogo.Search(hibikeonlinestream.SearchOptions{
|
||||
Query: tt.query,
|
||||
Dub: tt.dubbed,
|
||||
})
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, results)
|
||||
|
||||
for _, r := range results {
|
||||
assert.NotEmpty(t, r.ID, "ID is empty")
|
||||
assert.NotEmpty(t, r.Title, "Title is empty")
|
||||
assert.NotEmpty(t, r.URL, "URL is empty")
|
||||
}
|
||||
|
||||
spew.Dump(results)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGogoanime_FetchEpisodes(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
id: "one-piece",
|
||||
},
|
||||
{
|
||||
name: "One Piece (Dub)",
|
||||
id: "one-piece-dub",
|
||||
},
|
||||
}
|
||||
|
||||
gogo := NewGogoanime(util.NewLogger())
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
episodes, err := gogo.FindEpisodes(tt.id)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, episodes)
|
||||
|
||||
for _, e := range episodes {
|
||||
assert.NotEmpty(t, e.ID, "ID is empty")
|
||||
assert.NotEmpty(t, e.Number, "Number is empty")
|
||||
assert.NotEmpty(t, e.URL, "URL is empty")
|
||||
}
|
||||
|
||||
spew.Dump(episodes)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGogoanime_FetchSources(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
episode *hibikeonlinestream.EpisodeDetails
|
||||
server string
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-episode-1075",
|
||||
Number: 1075,
|
||||
URL: "https://anitaku.to/one-piece-episode-1075",
|
||||
},
|
||||
server: VidstreamingServer,
|
||||
},
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-episode-1075",
|
||||
Number: 1075,
|
||||
URL: "https://anitaku.to/one-piece-episode-1075",
|
||||
},
|
||||
server: StreamSBServer,
|
||||
},
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-episode-1075",
|
||||
Number: 1075,
|
||||
URL: "https://anitaku.to/one-piece-episode-1075",
|
||||
},
|
||||
server: GogocdnServer,
|
||||
},
|
||||
{
|
||||
name: "Bocchi the Rock!",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "bocchi-the-rock-episode-1",
|
||||
Number: 1075,
|
||||
URL: "https://anitaku.to/bocchi-the-rock-episode-1",
|
||||
},
|
||||
server: GogocdnServer,
|
||||
},
|
||||
}
|
||||
gogo := NewGogoanime(util.NewLogger())
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
sources, err := gogo.FindEpisodeServer(tt.episode, tt.server)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrSourceNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Source not found")
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, sources)
|
||||
|
||||
for _, s := range sources.VideoSources {
|
||||
assert.NotEmpty(t, s, "Source is empty")
|
||||
}
|
||||
|
||||
spew.Dump(sources)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
342
seanime-2.9.10/internal/onlinestream/providers/zoro.go
Normal file
342
seanime-2.9.10/internal/onlinestream/providers/zoro.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gocolly/colly"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/onlinestream/sources"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
type Zoro struct {
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
UserAgent string
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewZoro(logger *zerolog.Logger) hibikeonlinestream.Provider {
|
||||
return &Zoro{
|
||||
BaseURL: "https://hianime.to",
|
||||
UserAgent: util.GetRandomUserAgent(),
|
||||
Client: &http.Client{},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *Zoro) GetSettings() hibikeonlinestream.Settings {
|
||||
return hibikeonlinestream.Settings{
|
||||
EpisodeServers: []string{VidcloudServer, VidstreamingServer},
|
||||
SupportsDub: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *Zoro) Search(opts hibikeonlinestream.SearchOptions) ([]*hibikeonlinestream.SearchResult, error) {
|
||||
var results []*hibikeonlinestream.SearchResult
|
||||
|
||||
query := opts.Query
|
||||
dubbed := opts.Dub
|
||||
|
||||
z.logger.Debug().Str("query", query).Bool("dubbed", dubbed).Msg("zoro: Searching anime")
|
||||
|
||||
c := colly.NewCollector()
|
||||
|
||||
c.OnHTML(".flw-item", func(e *colly.HTMLElement) {
|
||||
id := strings.Split(strings.Split(e.ChildAttr(".film-name a", "href"), "/")[1], "?")[0]
|
||||
title := e.ChildText(".film-name a")
|
||||
url := strings.Split(z.BaseURL+e.ChildAttr(".film-name a", "href"), "?")[0]
|
||||
subOrDub := hibikeonlinestream.Sub
|
||||
foundSub := false
|
||||
foundDub := false
|
||||
if e.ChildText(".tick-item.tick-dub") != "" {
|
||||
foundDub = true
|
||||
}
|
||||
if e.ChildText(".tick-item.tick-sub") != "" {
|
||||
foundSub = true
|
||||
}
|
||||
if foundSub && foundDub {
|
||||
subOrDub = hibikeonlinestream.SubAndDub
|
||||
} else if foundDub {
|
||||
subOrDub = hibikeonlinestream.Dub
|
||||
}
|
||||
results = append(results, &hibikeonlinestream.SearchResult{
|
||||
ID: id,
|
||||
Title: title,
|
||||
URL: url,
|
||||
SubOrDub: subOrDub,
|
||||
})
|
||||
})
|
||||
|
||||
searchURL := z.BaseURL + "/search?keyword=" + url.QueryEscape(query)
|
||||
|
||||
err := c.Visit(searchURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dubbed {
|
||||
results = lo.Filter(results, func(r *hibikeonlinestream.SearchResult, _ int) bool {
|
||||
return r.SubOrDub == hibikeonlinestream.Dub || r.SubOrDub == hibikeonlinestream.SubAndDub
|
||||
})
|
||||
}
|
||||
|
||||
z.logger.Debug().Int("count", len(results)).Msg("zoro: Fetched anime")
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (z *Zoro) FindEpisodes(id string) ([]*hibikeonlinestream.EpisodeDetails, error) {
|
||||
var episodes []*hibikeonlinestream.EpisodeDetails
|
||||
|
||||
z.logger.Debug().Str("id", id).Msg("zoro: Fetching episodes")
|
||||
|
||||
c := colly.NewCollector()
|
||||
|
||||
subOrDub := hibikeonlinestream.Sub
|
||||
|
||||
c.OnHTML("div.film-stats > div.tick", func(e *colly.HTMLElement) {
|
||||
if e.ChildText(".tick-item.tick-dub") != "" {
|
||||
subOrDub = hibikeonlinestream.Dub
|
||||
}
|
||||
if e.ChildText(".tick-item.tick-sub") != "" {
|
||||
if subOrDub == hibikeonlinestream.Dub {
|
||||
subOrDub = hibikeonlinestream.SubAndDub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watchUrl := fmt.Sprintf("%s/watch/%s", z.BaseURL, id)
|
||||
err := c.Visit(watchUrl)
|
||||
if err != nil {
|
||||
z.logger.Error().Err(err).Msg("zoro: Failed to fetch episodes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get episodes
|
||||
|
||||
splitId := strings.Split(id, "-")
|
||||
idNum := splitId[len(splitId)-1]
|
||||
ajaxUrl := fmt.Sprintf("%s/ajax/v2/episode/list/%s", z.BaseURL, idNum)
|
||||
|
||||
c2 := colly.NewCollector(
|
||||
colly.UserAgent(z.UserAgent),
|
||||
)
|
||||
|
||||
c2.OnRequest(func(r *colly.Request) {
|
||||
r.Headers.Set("X-Requested-With", "XMLHttpRequest")
|
||||
r.Headers.Set("Referer", watchUrl)
|
||||
})
|
||||
|
||||
c2.OnResponse(func(r *colly.Response) {
|
||||
var jsonResponse map[string]interface{}
|
||||
err = json.Unmarshal(r.Body, &jsonResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(jsonResponse["html"].(string)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
content := doc.Find(".detail-infor-content")
|
||||
content.Find("a").Each(func(i int, s *goquery.Selection) {
|
||||
id := s.AttrOr("href", "")
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
hrefParts := strings.Split(s.AttrOr("href", ""), "/")
|
||||
if len(hrefParts) < 2 {
|
||||
return
|
||||
}
|
||||
if subOrDub == hibikeonlinestream.SubAndDub {
|
||||
subOrDub = "both"
|
||||
}
|
||||
id = fmt.Sprintf("%s$%s", strings.Replace(hrefParts[2], "?ep=", "$episode$", 1), subOrDub)
|
||||
epNumber, _ := strconv.Atoi(s.AttrOr("data-number", ""))
|
||||
url := z.BaseURL + s.AttrOr("href", "")
|
||||
title := s.AttrOr("title", "")
|
||||
episodes = append(episodes, &hibikeonlinestream.EpisodeDetails{
|
||||
Provider: ZoroProvider,
|
||||
ID: id,
|
||||
Number: epNumber,
|
||||
URL: url,
|
||||
Title: title,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
err = c2.Visit(ajaxUrl)
|
||||
if err != nil {
|
||||
z.logger.Error().Err(err).Msg("zoro: Failed to fetch episodes")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
z.logger.Debug().Int("count", len(episodes)).Msg("zoro: Fetched episodes")
|
||||
|
||||
return episodes, nil
|
||||
}
|
||||
|
||||
func (z *Zoro) FindEpisodeServer(episodeInfo *hibikeonlinestream.EpisodeDetails, server string) (*hibikeonlinestream.EpisodeServer, error) {
|
||||
var source *hibikeonlinestream.EpisodeServer
|
||||
|
||||
if server == DefaultServer {
|
||||
server = VidcloudServer
|
||||
}
|
||||
|
||||
z.logger.Debug().Str("server", server).Str("episodeID", episodeInfo.ID).Msg("zoro: Fetching server sources")
|
||||
|
||||
episodeParts := strings.Split(episodeInfo.ID, "$")
|
||||
|
||||
if len(episodeParts) < 3 {
|
||||
return nil, errors.New("invalid episode id")
|
||||
}
|
||||
|
||||
episodeID := fmt.Sprintf("%s?ep=%s", episodeParts[0], episodeParts[2])
|
||||
subOrDub := hibikeonlinestream.Sub
|
||||
if episodeParts[len(episodeParts)-1] == "dub" {
|
||||
subOrDub = hibikeonlinestream.Dub
|
||||
}
|
||||
|
||||
// Get server
|
||||
|
||||
var serverId string
|
||||
|
||||
c := colly.NewCollector(
|
||||
colly.UserAgent(z.UserAgent),
|
||||
)
|
||||
c.OnRequest(func(r *colly.Request) {
|
||||
r.Headers.Set("X-Requested-With", "XMLHttpRequest")
|
||||
})
|
||||
|
||||
c.OnResponse(func(r *colly.Response) {
|
||||
var jsonResponse map[string]interface{}
|
||||
err := json.Unmarshal(r.Body, &jsonResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(jsonResponse["html"].(string)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch server {
|
||||
case VidcloudServer:
|
||||
serverId = z.findServerId(doc, 4, subOrDub)
|
||||
case VidstreamingServer:
|
||||
serverId = z.findServerId(doc, 4, subOrDub)
|
||||
case StreamSBServer:
|
||||
serverId = z.findServerId(doc, 4, subOrDub)
|
||||
case StreamtapeServer:
|
||||
serverId = z.findServerId(doc, 4, subOrDub)
|
||||
}
|
||||
})
|
||||
|
||||
ajaxEpisodeUrl := fmt.Sprintf("%s/ajax/v2/episode/servers?episodeId=%s", z.BaseURL, strings.Split(episodeID, "?ep=")[1])
|
||||
if err := c.Visit(ajaxEpisodeUrl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if serverId == "" {
|
||||
return nil, ErrServerNotFound
|
||||
}
|
||||
|
||||
c2 := colly.NewCollector(
|
||||
colly.UserAgent(z.UserAgent),
|
||||
)
|
||||
c2.OnRequest(func(r *colly.Request) {
|
||||
r.Headers.Set("X-Requested-With", "XMLHttpRequest")
|
||||
})
|
||||
|
||||
c2.OnResponse(func(r *colly.Response) {
|
||||
var jsonResponse map[string]interface{}
|
||||
err := json.Unmarshal(r.Body, &jsonResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, ok := jsonResponse["link"].(string); !ok {
|
||||
return
|
||||
}
|
||||
switch server {
|
||||
case VidcloudServer, VidstreamingServer:
|
||||
megacloud := onlinestream_sources.NewMegaCloud()
|
||||
sources, err := megacloud.Extract(jsonResponse["link"].(string))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: ZoroProvider,
|
||||
Server: server,
|
||||
Headers: map[string]string{},
|
||||
VideoSources: sources,
|
||||
}
|
||||
case StreamtapeServer:
|
||||
streamtape := onlinestream_sources.NewStreamtape()
|
||||
sources, err := streamtape.Extract(jsonResponse["link"].(string))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: ZoroProvider,
|
||||
Server: server,
|
||||
Headers: map[string]string{
|
||||
"Referer": jsonResponse["link"].(string),
|
||||
"User-Agent": z.UserAgent,
|
||||
},
|
||||
VideoSources: sources,
|
||||
}
|
||||
case StreamSBServer:
|
||||
streamsb := onlinestream_sources.NewStreamSB()
|
||||
sources, err := streamsb.Extract(jsonResponse["link"].(string))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
source = &hibikeonlinestream.EpisodeServer{
|
||||
Provider: ZoroProvider,
|
||||
Server: server,
|
||||
Headers: map[string]string{
|
||||
"Referer": jsonResponse["link"].(string),
|
||||
"watchsb": "streamsb",
|
||||
"User-Agent": z.UserAgent,
|
||||
},
|
||||
VideoSources: sources,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Get sources
|
||||
serverSourceUrl := fmt.Sprintf("%s/ajax/v2/episode/sources?id=%s", z.BaseURL, serverId)
|
||||
if err := c2.Visit(serverSourceUrl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if source == nil {
|
||||
z.logger.Warn().Str("server", server).Msg("zoro: No sources found")
|
||||
return nil, ErrSourceNotFound
|
||||
}
|
||||
|
||||
z.logger.Debug().Str("server", server).Int("videoSources", len(source.VideoSources)).Msg("zoro: Fetched server sources")
|
||||
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func (z *Zoro) findServerId(doc *goquery.Document, idx int, subOrDub hibikeonlinestream.SubOrDub) string {
|
||||
var serverId string
|
||||
doc.Find(fmt.Sprintf("div.ps_-block.ps_-block-sub.servers-%s > div.ps__-list > div", subOrDub)).Each(func(i int, s *goquery.Selection) {
|
||||
_serverId := s.AttrOr("data-server-id", "")
|
||||
if serverId == "" {
|
||||
if _serverId == strconv.Itoa(idx) {
|
||||
serverId = s.AttrOr("data-id", "")
|
||||
}
|
||||
}
|
||||
})
|
||||
return serverId
|
||||
}
|
||||
194
seanime-2.9.10/internal/onlinestream/providers/zoro_test.go
Normal file
194
seanime-2.9.10/internal/onlinestream/providers/zoro_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package onlinestream_providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestZoro_Search(t *testing.T) {
|
||||
|
||||
logger := util.NewLogger()
|
||||
zoro := NewZoro(logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
dubbed bool
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
query: "One Piece",
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "Dungeon Meshi",
|
||||
query: "Dungeon Meshi",
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "Omoi, Omoware, Furi, Furare",
|
||||
query: "Omoi, Omoware, Furi, Furare",
|
||||
dubbed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
results, err := zoro.Search(hibikeonlinestream.SearchOptions{
|
||||
Query: tt.query,
|
||||
Dub: tt.dubbed,
|
||||
})
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, results)
|
||||
|
||||
for _, r := range results {
|
||||
assert.NotEmpty(t, r.ID, "ID is empty")
|
||||
assert.NotEmpty(t, r.Title, "Title is empty")
|
||||
assert.NotEmpty(t, r.URL, "URL is empty")
|
||||
}
|
||||
|
||||
spew.Dump(results)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestZoro_FetchEpisodes(t *testing.T) {
|
||||
logger := util.NewLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
id: "one-piece-100",
|
||||
},
|
||||
{
|
||||
name: "The Apothecary Diaries",
|
||||
id: "the-apothecary-diaries-18578",
|
||||
},
|
||||
}
|
||||
|
||||
zoro := NewZoro(logger)
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
episodes, err := zoro.FindEpisodes(tt.id)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, episodes)
|
||||
|
||||
for _, e := range episodes {
|
||||
assert.NotEmpty(t, e.ID, "ID is empty")
|
||||
assert.NotEmpty(t, e.Number, "Number is empty")
|
||||
assert.NotEmpty(t, e.URL, "URL is empty")
|
||||
}
|
||||
|
||||
spew.Dump(episodes)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestZoro_FetchSources(t *testing.T) {
|
||||
logger := util.NewLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
episode *hibikeonlinestream.EpisodeDetails
|
||||
server string
|
||||
}{
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-100$episode$120118$both",
|
||||
Number: 1095,
|
||||
URL: "https://hianime.to/watch/one-piece-100?ep=120118",
|
||||
},
|
||||
server: VidcloudServer,
|
||||
},
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-100$episode$120118$both",
|
||||
Number: 1095,
|
||||
URL: "https://hianime.to/watch/one-piece-100?ep=120118",
|
||||
},
|
||||
server: VidstreamingServer,
|
||||
},
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-100$episode$120118$both",
|
||||
Number: 1095,
|
||||
URL: "https://hianime.to/watch/one-piece-100?ep=120118",
|
||||
},
|
||||
server: StreamtapeServer,
|
||||
},
|
||||
{
|
||||
name: "One Piece",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "one-piece-100$episode$120118$both",
|
||||
Number: 1095,
|
||||
URL: "https://hianime.to/watch/one-piece-100?ep=120118",
|
||||
},
|
||||
server: StreamSBServer,
|
||||
},
|
||||
{
|
||||
name: "Apothecary Diaries",
|
||||
episode: &hibikeonlinestream.EpisodeDetails{
|
||||
ID: "the-apothecary-diaries-18578$episode$122954$sub",
|
||||
Number: 24,
|
||||
URL: "https://hianime.to/watch/the-apothecary-diaries-18578?ep=122954",
|
||||
},
|
||||
server: StreamSBServer,
|
||||
},
|
||||
}
|
||||
zoro := NewZoro(logger)
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
serverSources, err := zoro.FindEpisodeServer(tt.episode, tt.server)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrSourceNotFound) && !errors.Is(err, ErrServerNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, serverSources)
|
||||
|
||||
for _, s := range serverSources.VideoSources {
|
||||
assert.NotEmpty(t, s, "Source is empty")
|
||||
}
|
||||
|
||||
spew.Dump(serverSources)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
288
seanime-2.9.10/internal/onlinestream/repository.go
Normal file
288
seanime-2.9.10/internal/onlinestream/repository.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package onlinestream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/database/db"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/util/filecache"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type (
|
||||
Repository struct {
|
||||
logger *zerolog.Logger
|
||||
providerExtensionBank *extension.UnifiedBank
|
||||
fileCacher *filecache.Cacher
|
||||
metadataProvider metadata.Provider
|
||||
platform platform.Platform
|
||||
anilistBaseAnimeCache *anilist.BaseAnimeCache
|
||||
db *db.Database
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoVideoSourceFound = errors.New("no video source found")
|
||||
)
|
||||
|
||||
type (
|
||||
Episode struct {
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IsFiller bool `json:"isFiller,omitempty"`
|
||||
}
|
||||
|
||||
EpisodeSource struct {
|
||||
Number int `json:"number"`
|
||||
VideoSources []*VideoSource `json:"videoSources"`
|
||||
Subtitles []*Subtitle `json:"subtitles,omitempty"`
|
||||
}
|
||||
|
||||
VideoSource struct {
|
||||
Server string `json:"server"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Quality string `json:"quality"`
|
||||
}
|
||||
|
||||
EpisodeListResponse struct {
|
||||
Episodes []*Episode `json:"episodes"`
|
||||
Media *anilist.BaseAnime `json:"media"`
|
||||
}
|
||||
|
||||
Subtitle struct {
|
||||
URL string `json:"url"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
NewRepositoryOptions struct {
|
||||
Logger *zerolog.Logger
|
||||
FileCacher *filecache.Cacher
|
||||
MetadataProvider metadata.Provider
|
||||
Platform platform.Platform
|
||||
Database *db.Database
|
||||
}
|
||||
)
|
||||
|
||||
func NewRepository(opts *NewRepositoryOptions) *Repository {
|
||||
return &Repository{
|
||||
logger: opts.Logger,
|
||||
metadataProvider: opts.MetadataProvider,
|
||||
fileCacher: opts.FileCacher,
|
||||
providerExtensionBank: extension.NewUnifiedBank(),
|
||||
anilistBaseAnimeCache: anilist.NewBaseAnimeCache(),
|
||||
platform: opts.Platform,
|
||||
db: opts.Database,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) InitExtensionBank(bank *extension.UnifiedBank) {
|
||||
r.providerExtensionBank = bank
|
||||
|
||||
r.logger.Debug().Msg("onlinestream: Initialized provider extension bank")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// getFcEpisodeDataBucket returns a episode data bucket for the provider and mediaId.
|
||||
// "Episode data" refers to the episodeData struct
|
||||
//
|
||||
// e.g., onlinestream_zoro_episode-data_123
|
||||
func (r *Repository) getFcEpisodeDataBucket(provider string, mediaId int) filecache.Bucket {
|
||||
return filecache.NewBucket("onlinestream_"+provider+"_episode-data_"+strconv.Itoa(mediaId), time.Hour*24*2)
|
||||
}
|
||||
|
||||
// getFcEpisodeListBucket returns a episode data bucket for the provider and mediaId.
|
||||
// "Episode list" refers to a slice of onlinestream_providers.EpisodeDetails
|
||||
//
|
||||
// e.g., onlinestream_zoro_episode-list_123
|
||||
func (r *Repository) getFcEpisodeListBucket(provider string, mediaId int) filecache.Bucket {
|
||||
return filecache.NewBucket("onlinestream_"+provider+"_episode-data_"+strconv.Itoa(mediaId), time.Hour*24*1)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (r *Repository) getMedia(ctx context.Context, mId int) (*anilist.BaseAnime, error) {
|
||||
media, err := r.anilistBaseAnimeCache.GetOrSet(mId, func() (*anilist.BaseAnime, error) {
|
||||
media, err := r.platform.GetAnime(ctx, mId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return media, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetMedia(ctx context.Context, mId int) (*anilist.BaseAnime, error) {
|
||||
return r.getMedia(ctx, mId)
|
||||
}
|
||||
|
||||
func (r *Repository) EmptyCache(mediaId int) error {
|
||||
_ = r.fileCacher.RemoveAllBy(func(filename string) bool {
|
||||
return strings.HasPrefix(filename, "onlinestream_") && strings.Contains(filename, strconv.Itoa(mediaId))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetMediaEpisodes(provider string, media *anilist.BaseAnime, dubbed bool) ([]*Episode, error) {
|
||||
episodes := make([]*Episode, 0)
|
||||
|
||||
if provider == "" {
|
||||
return episodes, nil
|
||||
}
|
||||
|
||||
// +---------------------+
|
||||
// | Animap |
|
||||
// +---------------------+
|
||||
|
||||
//animeMetadata, err := r.metadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, mId)
|
||||
// //foundAnimeMetadata := err == nil && animeMetadata != nil
|
||||
//aw := r.metadataProvider.GetAnimeMetadataWrapper(media, animeMetadata)
|
||||
|
||||
episodeCollection, err := anime.NewEpisodeCollection(anime.NewEpisodeCollectionOptions{
|
||||
AnimeMetadata: nil,
|
||||
Media: media,
|
||||
MetadataProvider: r.metadataProvider,
|
||||
Logger: r.logger,
|
||||
})
|
||||
foundEpisodeCollection := err == nil && episodeCollection != nil
|
||||
|
||||
// +---------------------+
|
||||
// | Episode list |
|
||||
// +---------------------+
|
||||
|
||||
// Fetch the episode list from the provider
|
||||
// "from" and "to" are set to 0 in order not to fetch episode servers
|
||||
ec, err := r.getEpisodeContainer(provider, media, 0, 0, dubbed, media.GetStartYearSafe())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, episodeDetails := range ec.ProviderEpisodeList {
|
||||
|
||||
// If the title contains "[{", it means it's an episode part (e.g. "Episode 6 [{6.5}]", the episode number should be 6)
|
||||
if strings.Contains(episodeDetails.Title, "[{") {
|
||||
ep := strings.Split(episodeDetails.Title, "[{")[1]
|
||||
ep = strings.Split(ep, "}]")[0]
|
||||
episodes = append(episodes, &Episode{
|
||||
Number: episodeDetails.Number,
|
||||
Title: fmt.Sprintf("Episode %s", ep),
|
||||
Image: media.GetBannerImageSafe(),
|
||||
Description: "",
|
||||
IsFiller: false,
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
if foundEpisodeCollection {
|
||||
episode, found := episodeCollection.FindEpisodeByNumber(episodeDetails.Number)
|
||||
if found {
|
||||
episodes = append(episodes, &Episode{
|
||||
Number: episodeDetails.Number,
|
||||
Title: episode.EpisodeTitle,
|
||||
Image: episode.EpisodeMetadata.Image,
|
||||
Description: episode.EpisodeMetadata.Summary,
|
||||
IsFiller: episode.EpisodeMetadata.IsFiller,
|
||||
})
|
||||
} else {
|
||||
episodes = append(episodes, &Episode{
|
||||
Number: episodeDetails.Number,
|
||||
Title: episodeDetails.Title,
|
||||
Image: media.GetCoverImageSafe(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
episodes = append(episodes, &Episode{
|
||||
Number: episodeDetails.Number,
|
||||
Title: episodeDetails.Title,
|
||||
Image: media.GetCoverImageSafe(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
episodes = lo.Filter(episodes, func(item *Episode, index int) bool {
|
||||
return item != nil
|
||||
})
|
||||
|
||||
return episodes, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetEpisodeSources(ctx context.Context, provider string, mId int, number int, dubbed bool, year int) (*EpisodeSource, error) {
|
||||
|
||||
// +---------------------+
|
||||
// | Media |
|
||||
// +---------------------+
|
||||
|
||||
media, err := r.getMedia(ctx, mId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// +---------------------+
|
||||
// | Episode servers |
|
||||
// +---------------------+
|
||||
|
||||
ec, err := r.getEpisodeContainer(provider, media, number, number, dubbed, year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sources *EpisodeSource
|
||||
for _, ep := range ec.Episodes {
|
||||
if ep.Number == number {
|
||||
s := &EpisodeSource{
|
||||
Number: ep.Number,
|
||||
VideoSources: make([]*VideoSource, 0),
|
||||
}
|
||||
for _, es := range ep.Servers {
|
||||
|
||||
for _, vs := range es.VideoSources {
|
||||
s.VideoSources = append(s.VideoSources, &VideoSource{
|
||||
Server: es.Server,
|
||||
Headers: es.Headers,
|
||||
URL: vs.URL,
|
||||
Quality: vs.Quality,
|
||||
})
|
||||
// Add subtitles if available
|
||||
// Subtitles are stored in each video source, but they are the same, so only add them once.
|
||||
if len(vs.Subtitles) > 0 && s.Subtitles == nil {
|
||||
s.Subtitles = make([]*Subtitle, 0, len(vs.Subtitles))
|
||||
for _, sub := range vs.Subtitles {
|
||||
s.Subtitles = append(s.Subtitles, &Subtitle{
|
||||
URL: sub.URL,
|
||||
Language: sub.Language,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sources = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sources == nil {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
357
seanime-2.9.10/internal/onlinestream/repository_actions.go
Normal file
357
seanime-2.9.10/internal/onlinestream/repository_actions.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package onlinestream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/extension"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
onlinestream_providers "seanime/internal/onlinestream/providers"
|
||||
"seanime/internal/util/comparison"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAnimeFound = errors.New("anime not found, try manual matching")
|
||||
ErrNoEpisodes = errors.New("no episodes found")
|
||||
errNoEpisodeSourceFound = errors.New("no source found for episode")
|
||||
)
|
||||
|
||||
type (
|
||||
// episodeContainer contains results of fetching the episodes from the provider.
|
||||
episodeContainer struct {
|
||||
Provider string
|
||||
// List of episode details from the provider.
|
||||
// It is used to get the episode servers.
|
||||
ProviderEpisodeList []*hibikeonlinestream.EpisodeDetails
|
||||
// List of episodes with their servers.
|
||||
Episodes []*episodeData
|
||||
}
|
||||
|
||||
// episodeData contains some details about a provider episode and all available servers.
|
||||
episodeData struct {
|
||||
Provider string
|
||||
ID string
|
||||
Number int
|
||||
Title string
|
||||
Servers []*hibikeonlinestream.EpisodeServer
|
||||
}
|
||||
)
|
||||
|
||||
// getEpisodeContainer gets the episode details and servers from the specified provider.
|
||||
// It takes the media ID, titles in order to fetch the episode details.
|
||||
// - This function can be used to only get the episode details by setting 'from' and 'to' to 0.
|
||||
//
|
||||
// Since the episode details are cached, we can request episode servers multiple times without fetching the episode details again.
|
||||
func (r *Repository) getEpisodeContainer(provider string, media *anilist.BaseAnime, from int, to int, dubbed bool, year int) (*episodeContainer, error) {
|
||||
|
||||
r.logger.Debug().
|
||||
Str("provider", provider).
|
||||
Int("mediaId", media.ID).
|
||||
Int("from", from).
|
||||
Int("to", to).
|
||||
Bool("dubbed", dubbed).
|
||||
Msg("onlinestream: Getting episode container")
|
||||
|
||||
// Key identifying the provider episode list in the file cache.
|
||||
// It includes "dubbed" because Gogoanime has a different entry for dubbed anime.
|
||||
// e.g. 1$provider$true
|
||||
providerEpisodeListKey := fmt.Sprintf("%d$%s$%v", media.ID, provider, dubbed)
|
||||
|
||||
// Create the episode container
|
||||
ec := &episodeContainer{
|
||||
Provider: provider,
|
||||
Episodes: make([]*episodeData, 0),
|
||||
ProviderEpisodeList: make([]*hibikeonlinestream.EpisodeDetails, 0),
|
||||
}
|
||||
|
||||
// Get the episode details from the provider.
|
||||
r.logger.Debug().
|
||||
Str("key", providerEpisodeListKey).
|
||||
Msgf("onlinestream: Fetching %s episode list", provider)
|
||||
|
||||
// Buckets for caching the episode list and episode data.
|
||||
fcEpisodeListBucket := r.getFcEpisodeListBucket(provider, media.ID)
|
||||
fcEpisodeDataBucket := r.getFcEpisodeDataBucket(provider, media.ID)
|
||||
|
||||
// Check if the episode list is cached to avoid fetching it again.
|
||||
var providerEpisodeList []*hibikeonlinestream.EpisodeDetails
|
||||
if found, _ := r.fileCacher.Get(fcEpisodeListBucket, providerEpisodeListKey, &providerEpisodeList); !found {
|
||||
var err error
|
||||
providerEpisodeList, err = r.getProviderEpisodeList(provider, media, dubbed, year)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to get provider episodes")
|
||||
return nil, err // ErrNoAnimeFound or ErrNoEpisodes
|
||||
}
|
||||
_ = r.fileCacher.Set(fcEpisodeListBucket, providerEpisodeListKey, providerEpisodeList)
|
||||
} else {
|
||||
r.logger.Debug().
|
||||
Str("key", providerEpisodeListKey).
|
||||
Msg("onlinestream: Cache HIT for episode list")
|
||||
}
|
||||
|
||||
ec.ProviderEpisodeList = providerEpisodeList
|
||||
|
||||
var lastServerError error
|
||||
|
||||
for _, episodeDetails := range providerEpisodeList {
|
||||
|
||||
if episodeDetails.Number >= from && episodeDetails.Number <= to {
|
||||
|
||||
// Check if the episode is cached to avoid fetching the sources again.
|
||||
key := fmt.Sprintf("%d$%s$%d$%v", media.ID, provider, episodeDetails.Number, dubbed)
|
||||
|
||||
r.logger.Debug().
|
||||
Str("key", key).
|
||||
Msgf("onlinestream: Fetching episode '%d' servers", episodeDetails.Number)
|
||||
|
||||
// Check episode cache
|
||||
var cached *episodeData
|
||||
if found, _ := r.fileCacher.Get(fcEpisodeDataBucket, key, &cached); found {
|
||||
ec.Episodes = append(ec.Episodes, cached)
|
||||
|
||||
r.logger.Debug().
|
||||
Str("key", key).
|
||||
Msgf("onlinestream: Cache HIT for episode '%d' servers", episodeDetails.Number)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Zoro dubs
|
||||
if provider == onlinestream_providers.ZoroProvider && dubbed {
|
||||
// If the episode details have both sub and dub, we need to get the dub episode.
|
||||
if !strings.HasSuffix(episodeDetails.ID, string(hibikeonlinestream.SubAndDub)) {
|
||||
// Skip sub-only episodes
|
||||
continue
|
||||
}
|
||||
// Replace "both" with "dub" so that [getProviderEpisodeServers] can find the dub episode.
|
||||
episodeDetails.ID = strings.Replace(episodeDetails.ID, string(hibikeonlinestream.SubAndDub), string(hibikeonlinestream.Dub), 1)
|
||||
}
|
||||
|
||||
// Fetch episode servers
|
||||
servers, err := r.getProviderEpisodeServers(provider, episodeDetails)
|
||||
if err != nil {
|
||||
lastServerError = err
|
||||
r.logger.Error().Err(err).Msgf("onlinestream: failed to get episode '%d' servers", episodeDetails.Number)
|
||||
continue
|
||||
}
|
||||
|
||||
episode := &episodeData{
|
||||
ID: episodeDetails.ID,
|
||||
Number: episodeDetails.Number,
|
||||
Title: episodeDetails.Title,
|
||||
Servers: servers,
|
||||
}
|
||||
ec.Episodes = append(ec.Episodes, episode)
|
||||
|
||||
r.logger.Debug().
|
||||
Str("key", key).
|
||||
Msgf("onlinestream: Found %d servers for episode '%d'", len(servers), episodeDetails.Number)
|
||||
|
||||
_ = r.fileCacher.Set(fcEpisodeDataBucket, key, episode)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if from > 0 && to > 0 && len(ec.Episodes) == 0 {
|
||||
r.logger.Error().Err(lastServerError).Msg("onlinestream: No episode servers found")
|
||||
return nil, fmt.Errorf("no episode servers found, provider returned: '%w'", lastServerError)
|
||||
}
|
||||
|
||||
if len(ec.ProviderEpisodeList) == 0 {
|
||||
r.logger.Error().Msg("onlinestream: No episodes found for this anime")
|
||||
return nil, fmt.Errorf("no episodes found for this anime")
|
||||
}
|
||||
|
||||
return ec, nil
|
||||
}
|
||||
|
||||
// getProviderEpisodeServers gets all the available servers for the episode.
|
||||
// It returns errNoEpisodeSourceFound if no sources are found.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// episodeDetails, _ := getProviderEpisodeListFromTitles(provider, titles, dubbed)
|
||||
// episodeServers, err := getProviderEpisodeServers(provider, episodeDetails[0])
|
||||
func (r *Repository) getProviderEpisodeServers(provider string, episodeDetails *hibikeonlinestream.EpisodeDetails) ([]*hibikeonlinestream.EpisodeServer, error) {
|
||||
var providerServers []*hibikeonlinestream.EpisodeServer
|
||||
|
||||
providerExtension, ok := extension.GetExtension[extension.OnlinestreamProviderExtension](r.providerExtensionBank, provider)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("provider extension '%s' not found", provider)
|
||||
}
|
||||
|
||||
for _, episodeServer := range providerExtension.GetProvider().GetSettings().EpisodeServers {
|
||||
res, err := providerExtension.GetProvider().FindEpisodeServer(episodeDetails, episodeServer)
|
||||
if err == nil {
|
||||
// Add the server to the list for the episode
|
||||
providerServers = append(providerServers, res)
|
||||
}
|
||||
}
|
||||
|
||||
if len(providerServers) == 0 {
|
||||
return nil, errNoEpisodeSourceFound
|
||||
}
|
||||
|
||||
return providerServers, nil
|
||||
}
|
||||
|
||||
// getProviderEpisodeList gets all the hibikeonlinestream.EpisodeDetails from the provider based on the anime's titles.
|
||||
// It returns ErrNoAnimeFound if the anime is not found or ErrNoEpisodes if no episodes are found.
|
||||
func (r *Repository) getProviderEpisodeList(provider string, media *anilist.BaseAnime, dubbed bool, year int) ([]*hibikeonlinestream.EpisodeDetails, error) {
|
||||
var ret []*hibikeonlinestream.EpisodeDetails
|
||||
// romajiTitle := strings.ReplaceAll(media.GetEnglishTitleSafe(), ":", "")
|
||||
// englishTitle := strings.ReplaceAll(media.GetRomajiTitleSafe(), ":", "")
|
||||
|
||||
romajiTitle := media.GetRomajiTitleSafe()
|
||||
englishTitle := media.GetEnglishTitleSafe()
|
||||
|
||||
providerExtension, ok := extension.GetExtension[extension.OnlinestreamProviderExtension](r.providerExtensionBank, provider)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("provider extension '%s' not found", provider)
|
||||
}
|
||||
|
||||
mId := media.ID
|
||||
|
||||
var matchId string
|
||||
|
||||
// +---------------------+
|
||||
// | Database |
|
||||
// +---------------------+
|
||||
|
||||
// Search for the mapping in the database
|
||||
mapping, found := r.db.GetOnlinestreamMapping(provider, mId)
|
||||
if found {
|
||||
r.logger.Debug().Str("animeId", mapping.AnimeID).Msg("onlinestream: Using manual mapping")
|
||||
matchId = mapping.AnimeID
|
||||
}
|
||||
|
||||
if matchId == "" {
|
||||
// +---------------------+
|
||||
// | Search |
|
||||
// +---------------------+
|
||||
|
||||
// Get search results.
|
||||
var searchResults []*hibikeonlinestream.SearchResult
|
||||
|
||||
queryMedia := hibikeonlinestream.Media{
|
||||
ID: media.ID,
|
||||
IDMal: media.GetIDMal(),
|
||||
Status: string(*media.GetStatus()),
|
||||
Format: string(*media.GetFormat()),
|
||||
EnglishTitle: media.GetTitle().GetEnglish(),
|
||||
RomajiTitle: media.GetRomajiTitleSafe(),
|
||||
EpisodeCount: media.GetTotalEpisodeCount(),
|
||||
Synonyms: media.GetSynonymsContainingSeason(),
|
||||
IsAdult: *media.GetIsAdult(),
|
||||
StartDate: &hibikeonlinestream.FuzzyDate{
|
||||
Year: *media.GetStartDate().GetYear(),
|
||||
Month: media.GetStartDate().GetMonth(),
|
||||
Day: media.GetStartDate().GetDay(),
|
||||
},
|
||||
}
|
||||
|
||||
added := make(map[string]struct{})
|
||||
|
||||
if romajiTitle != "" {
|
||||
// Search by romaji title
|
||||
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
Media: queryMedia,
|
||||
Query: romajiTitle,
|
||||
Dub: dubbed,
|
||||
Year: year,
|
||||
})
|
||||
if err == nil && len(res) > 0 {
|
||||
searchResults = append(searchResults, res...)
|
||||
for _, r := range res {
|
||||
added[r.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to search for romaji title")
|
||||
}
|
||||
r.logger.Debug().
|
||||
Int("romajiTitleResults", len(res)).
|
||||
Msg("onlinestream: Found results for romaji title")
|
||||
}
|
||||
|
||||
if englishTitle != "" {
|
||||
// Search by english title
|
||||
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
Media: queryMedia,
|
||||
Query: englishTitle,
|
||||
Dub: dubbed,
|
||||
Year: year,
|
||||
})
|
||||
if err == nil && len(res) > 0 {
|
||||
for _, r := range res {
|
||||
if _, ok := added[r.ID]; !ok {
|
||||
searchResults = append(searchResults, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to search for english title")
|
||||
}
|
||||
r.logger.Debug().
|
||||
Int("englishTitleResults", len(res)).
|
||||
Msg("onlinestream: Found results for english title")
|
||||
}
|
||||
|
||||
if len(searchResults) == 0 {
|
||||
return nil, fmt.Errorf("automatic matching returned no results")
|
||||
}
|
||||
|
||||
bestResult, found := GetBestSearchResult(searchResults, media.GetAllTitles())
|
||||
if !found {
|
||||
return nil, ErrNoAnimeFound
|
||||
}
|
||||
matchId = bestResult.ID
|
||||
}
|
||||
|
||||
// Fetch episodes.
|
||||
ret, err := providerExtension.GetProvider().FindEpisodes(matchId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("provider returned an error: %w", err)
|
||||
}
|
||||
|
||||
if len(ret) == 0 {
|
||||
return nil, fmt.Errorf("provider returned no episodes")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func GetBestSearchResult(searchResults []*hibikeonlinestream.SearchResult, titles []*string) (*hibikeonlinestream.SearchResult, bool) {
|
||||
// Filter results to get the best match.
|
||||
compBestResults := make([]*comparison.LevenshteinResult, 0, len(searchResults))
|
||||
for _, r := range searchResults {
|
||||
// Compare search result title with all titles.
|
||||
compBestResult, found := comparison.FindBestMatchWithLevenshtein(&r.Title, titles)
|
||||
if found {
|
||||
compBestResults = append(compBestResults, compBestResult)
|
||||
}
|
||||
}
|
||||
|
||||
if len(compBestResults) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
compBestResult := compBestResults[0]
|
||||
for _, r := range compBestResults {
|
||||
if r.Distance < compBestResult.Distance {
|
||||
compBestResult = r
|
||||
}
|
||||
}
|
||||
|
||||
// Get most accurate search result.
|
||||
var bestResult *hibikeonlinestream.SearchResult
|
||||
for _, r := range searchResults {
|
||||
if r.Title == *compBestResult.OriginalValue {
|
||||
bestResult = r
|
||||
break
|
||||
}
|
||||
}
|
||||
return bestResult, true
|
||||
}
|
||||
112
seanime-2.9.10/internal/onlinestream/repository_actions_test.go
Normal file
112
seanime-2.9.10/internal/onlinestream/repository_actions_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package onlinestream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"seanime/internal/api/anilist"
|
||||
onlinestream_providers "seanime/internal/onlinestream/providers"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/filecache"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOnlineStream_GetEpisodes(t *testing.T) {
|
||||
t.Skip("TODO: Fix this test by loading built-in extensions")
|
||||
test_utils.SetTwoLevelDeep()
|
||||
test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
anilistClient := anilist.TestGetMockAnilistClient()
|
||||
|
||||
//fileCacher, _ := filecache.NewCacher(filepath.Join(test_utils.ConfigData.Path.DataDir, "cache"))
|
||||
fileCacher, _ := filecache.NewCacher(filepath.Join(tempDir, "cache"))
|
||||
|
||||
os := NewRepository(&NewRepositoryOptions{
|
||||
Logger: util.NewLogger(),
|
||||
FileCacher: fileCacher,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaId int
|
||||
from int
|
||||
to int
|
||||
provider string
|
||||
dubbed bool
|
||||
}{
|
||||
{
|
||||
name: "Cowboy Bebop",
|
||||
mediaId: 1,
|
||||
from: 1,
|
||||
to: 2,
|
||||
provider: onlinestream_providers.GogoanimeProvider,
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "Cowboy Bebop",
|
||||
mediaId: 1,
|
||||
from: 1,
|
||||
to: 2,
|
||||
provider: onlinestream_providers.ZoroProvider,
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "One Piece",
|
||||
mediaId: 21,
|
||||
from: 1075,
|
||||
to: 1076,
|
||||
provider: onlinestream_providers.ZoroProvider,
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "Dungeon Meshi",
|
||||
mediaId: 153518,
|
||||
from: 1,
|
||||
to: 1,
|
||||
provider: onlinestream_providers.ZoroProvider,
|
||||
dubbed: false,
|
||||
},
|
||||
{
|
||||
name: "Omoi, Omoware, Furi, Furare",
|
||||
mediaId: 109125,
|
||||
from: 1,
|
||||
to: 1,
|
||||
provider: onlinestream_providers.ZoroProvider,
|
||||
dubbed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
mediaF, err := anilistClient.BaseAnimeByID(context.Background(), &tt.mediaId)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't get media: %s", err)
|
||||
}
|
||||
media := mediaF.GetMedia()
|
||||
|
||||
ec, err := os.getEpisodeContainer(tt.provider, media, tt.from, tt.to, tt.dubbed, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't find episodes, %s", err)
|
||||
}
|
||||
|
||||
t.Logf("Provider: %s, found %d episodes for the anime", ec.Provider, len(ec.ProviderEpisodeList))
|
||||
// Episode Data
|
||||
for _, ep := range ec.Episodes {
|
||||
t.Logf("\t\tEpisode %d has %d servers", ep.Number, len(ep.Servers))
|
||||
for _, s := range ep.Servers {
|
||||
t.Logf("\t\t\tServer: %s", s.Server)
|
||||
for _, vs := range s.VideoSources {
|
||||
t.Logf("\t\t\t\tVideo Source: %s, Type: %s", vs.Quality, vs.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
24
seanime-2.9.10/internal/onlinestream/sources/common.go
Normal file
24
seanime-2.9.10/internal/onlinestream/sources/common.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package onlinestream_sources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoVideoSourceFound = errors.New("no episode source found")
|
||||
ErrVideoSourceExtraction = errors.New("error while extracting video sources")
|
||||
)
|
||||
|
||||
type VideoExtractor interface {
|
||||
Extract(uri string) ([]*hibikeonlinestream.VideoSource, error)
|
||||
}
|
||||
|
||||
const (
|
||||
QualityDefault = "default"
|
||||
QualityAuto = "auto"
|
||||
Quality360 = "360"
|
||||
Quality480 = "480"
|
||||
Quality720 = "720"
|
||||
Quality1080 = "1080"
|
||||
)
|
||||
277
seanime-2.9.10/internal/onlinestream/sources/gogocdn.go
Normal file
277
seanime-2.9.10/internal/onlinestream/sources/gogocdn.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package onlinestream_sources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gocolly/colly"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
type cdnKeys struct {
|
||||
key []byte
|
||||
secondKey []byte
|
||||
iv []byte
|
||||
}
|
||||
|
||||
type GogoCDN struct {
|
||||
client *http.Client
|
||||
serverName string
|
||||
keys cdnKeys
|
||||
referrer string
|
||||
}
|
||||
|
||||
func NewGogoCDN() *GogoCDN {
|
||||
return &GogoCDN{
|
||||
client: &http.Client{},
|
||||
serverName: "goload",
|
||||
keys: cdnKeys{
|
||||
key: []byte("37911490979715163134003223491201"),
|
||||
secondKey: []byte("54674138327930866480207815084989"),
|
||||
iv: []byte("3134003223491201"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Extract fetches and extracts video sources from the provided URI.
|
||||
func (g *GogoCDN) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) {
|
||||
|
||||
defer util.HandlePanicInModuleThen("onlinestream/sources/gogocdn/Extract", func() {
|
||||
err = ErrVideoSourceExtraction
|
||||
})
|
||||
|
||||
// Instantiate a new collector
|
||||
c := colly.NewCollector(
|
||||
// Allow visiting the same page multiple times
|
||||
colly.AllowURLRevisit(),
|
||||
)
|
||||
ur, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Variables to hold extracted values
|
||||
var scriptValue, id string
|
||||
|
||||
id = ur.Query().Get("id")
|
||||
|
||||
// Find and extract the script value and id
|
||||
c.OnHTML("script[data-name='episode']", func(e *colly.HTMLElement) {
|
||||
scriptValue = e.Attr("data-value")
|
||||
|
||||
})
|
||||
|
||||
// Start scraping
|
||||
err = c.Visit(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if scriptValue and id are found
|
||||
if scriptValue == "" || id == "" {
|
||||
return nil, errors.New("script value or id not found")
|
||||
}
|
||||
|
||||
// Extract video sources
|
||||
ajaxUrl := fmt.Sprintf("%s://%s/encrypt-ajax.php?%s", ur.Scheme, ur.Host, g.generateEncryptedAjaxParams(id, scriptValue))
|
||||
|
||||
req, err := http.NewRequest("GET", ajaxUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
|
||||
encryptedData, err := g.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer encryptedData.Body.Close()
|
||||
|
||||
encryptedDataBytesRes, err := io.ReadAll(encryptedData.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var encryptedDataBytes map[string]string
|
||||
err = json.Unmarshal(encryptedDataBytesRes, &encryptedDataBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := g.decryptAjaxData(encryptedDataBytes["data"])
|
||||
|
||||
source, ok := data["source"].([]interface{})
|
||||
|
||||
// Check if source is found
|
||||
if !ok {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
|
||||
var results []*hibikeonlinestream.VideoSource
|
||||
|
||||
urls := make([]string, 0)
|
||||
for _, src := range source {
|
||||
s := src.(map[string]interface{})
|
||||
urls = append(urls, s["file"].(string))
|
||||
}
|
||||
|
||||
sourceBK, ok := data["source_bk"].([]interface{})
|
||||
if ok {
|
||||
for _, src := range sourceBK {
|
||||
s := src.(map[string]interface{})
|
||||
urls = append(urls, s["file"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
|
||||
vs, ok := g.urlToVideoSource(url, source, sourceBK)
|
||||
if ok {
|
||||
results = append(results, vs...)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (g *GogoCDN) urlToVideoSource(url string, source []interface{}, sourceBK []interface{}) (vs []*hibikeonlinestream.VideoSource, ok bool) {
|
||||
defer util.HandlePanicInModuleThen("onlinestream/sources/gogocdn/urlToVideoSource", func() {
|
||||
ok = false
|
||||
})
|
||||
ret := make([]*hibikeonlinestream.VideoSource, 0)
|
||||
if strings.Contains(url, ".m3u8") {
|
||||
resResult, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer resResult.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resResult.Body)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
resolutions := regexp.MustCompile(`(RESOLUTION=)(.*)(\s*?)(\s.*)`).FindAllStringSubmatch(bodyString, -1)
|
||||
baseURL := url[:strings.LastIndex(url, "/")]
|
||||
|
||||
for _, res := range resolutions {
|
||||
quality := strings.Split(strings.Split(res[2], "x")[1], ",")[0]
|
||||
url := fmt.Sprintf("%s/%s", baseURL, strings.TrimSpace(res[4]))
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceM3U8, Quality: quality + "p"})
|
||||
}
|
||||
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceM3U8, Quality: "default"})
|
||||
} else {
|
||||
for _, src := range source {
|
||||
s := src.(map[string]interface{})
|
||||
if s["file"].(string) == url {
|
||||
quality := strings.Split(s["label"].(string), " ")[0] + "p"
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceMP4, Quality: quality})
|
||||
}
|
||||
}
|
||||
if sourceBK != nil {
|
||||
for _, src := range sourceBK {
|
||||
s := src.(map[string]interface{})
|
||||
if s["file"].(string) == url {
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{URL: url, Type: hibikeonlinestream.VideoSourceMP4, Quality: "backup"})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// generateEncryptedAjaxParams generates encrypted AJAX parameters.
|
||||
func (g *GogoCDN) generateEncryptedAjaxParams(id, scriptValue string) string {
|
||||
encryptedKey := g.encrypt(id, g.keys.iv, g.keys.key)
|
||||
decryptedToken := g.decrypt(scriptValue, g.keys.iv, g.keys.key)
|
||||
return fmt.Sprintf("id=%s&alias=%s", encryptedKey, decryptedToken)
|
||||
}
|
||||
|
||||
// encrypt encrypts the given text using AES CBC mode.
|
||||
func (g *GogoCDN) encrypt(text string, iv []byte, key []byte) string {
|
||||
block, _ := aes.NewCipher(key)
|
||||
textBytes := []byte(text)
|
||||
textBytes = pkcs7Padding(textBytes, aes.BlockSize)
|
||||
cipherText := make([]byte, len(textBytes))
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(cipherText, textBytes)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(cipherText)
|
||||
}
|
||||
|
||||
// decrypt decrypts the given text using AES CBC mode.
|
||||
func (g *GogoCDN) decrypt(text string, iv []byte, key []byte) string {
|
||||
block, _ := aes.NewCipher(key)
|
||||
cipherText, _ := base64.StdEncoding.DecodeString(text)
|
||||
plainText := make([]byte, len(cipherText))
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(plainText, cipherText)
|
||||
plainText = pkcs7Trimming(plainText)
|
||||
|
||||
return string(plainText)
|
||||
}
|
||||
|
||||
func (g *GogoCDN) decryptAjaxData(encryptedData string) (map[string]interface{}, error) {
|
||||
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(g.keys.secondKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(decodedData) < aes.BlockSize {
|
||||
return nil, fmt.Errorf("cipher text too short")
|
||||
}
|
||||
|
||||
iv := g.keys.iv
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(decodedData, decodedData)
|
||||
|
||||
// Remove padding
|
||||
decodedData = pkcs7Trimming(decodedData)
|
||||
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(decodedData, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// pkcs7Padding pads the text to be a multiple of blockSize using Pkcs7 padding.
|
||||
func pkcs7Padding(text []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(text)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(text, padText...)
|
||||
}
|
||||
|
||||
// pkcs7Trimming removes Pkcs7 padding from the text.
|
||||
func pkcs7Trimming(text []byte) []byte {
|
||||
length := len(text)
|
||||
unpadding := int(text[length-1])
|
||||
return text[:(length - unpadding)]
|
||||
}
|
||||
16
seanime-2.9.10/internal/onlinestream/sources/gogocdn_test.go
Normal file
16
seanime-2.9.10/internal/onlinestream/sources/gogocdn_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package onlinestream_sources
|
||||
|
||||
import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGogoCDN_Extract(t *testing.T) {
|
||||
gogo := NewGogoCDN()
|
||||
|
||||
ret, err := gogo.Extract("https://embtaku.pro/streaming.php?id=MjExNjU5&title=One+Piece+Episode+1075")
|
||||
assert.NoError(t, err)
|
||||
|
||||
spew.Dump(ret)
|
||||
}
|
||||
350
seanime-2.9.10/internal/onlinestream/sources/megacloud.go
Normal file
350
seanime-2.9.10/internal/onlinestream/sources/megacloud.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package onlinestream_sources
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MegaCloud struct {
|
||||
Script string
|
||||
Sources string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
func NewMegaCloud() *MegaCloud {
|
||||
return &MegaCloud{
|
||||
Script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js",
|
||||
Sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=",
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MegaCloud) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) {
|
||||
defer util.HandlePanicInModuleThen("onlinestream/sources/megacloud/Extract", func() {
|
||||
err = ErrVideoSourceExtraction
|
||||
})
|
||||
|
||||
videoIdParts := strings.Split(uri, "/")
|
||||
videoId := videoIdParts[len(videoIdParts)-1]
|
||||
videoId = strings.Split(videoId, "?")[0]
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", m.Sources+videoId, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
req.Header.Set("User-Agent", m.UserAgent)
|
||||
req.Header.Set("Referer", uri)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var srcData map[string]interface{}
|
||||
err = json.NewDecoder(res.Body).Decode(&srcData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subtitles := make([]*hibikeonlinestream.VideoSubtitle, 0)
|
||||
for idx, s := range srcData["tracks"].([]interface{}) {
|
||||
sub := s.(map[string]interface{})
|
||||
label, ok := sub["label"].(string)
|
||||
if ok {
|
||||
subtitle := &hibikeonlinestream.VideoSubtitle{
|
||||
URL: sub["file"].(string),
|
||||
ID: label,
|
||||
Language: label,
|
||||
IsDefault: idx == 0,
|
||||
}
|
||||
subtitles = append(subtitles, subtitle)
|
||||
}
|
||||
}
|
||||
if encryptedString, ok := srcData["sources"]; ok {
|
||||
|
||||
switch encryptedString.(type) {
|
||||
case []interface{}:
|
||||
if len(encryptedString.([]interface{})) == 0 {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
videoSources := make([]*hibikeonlinestream.VideoSource, 0)
|
||||
if e, ok := encryptedString.([]interface{})[0].(map[string]interface{}); ok {
|
||||
file, ok := e["file"].(string)
|
||||
if ok {
|
||||
videoSources = append(videoSources, &hibikeonlinestream.VideoSource{
|
||||
URL: file,
|
||||
Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(file, ".m3u8")],
|
||||
Subtitles: subtitles,
|
||||
Quality: QualityAuto,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(videoSources) == 0 {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
|
||||
return videoSources, nil
|
||||
|
||||
case []map[string]interface{}:
|
||||
if srcData["encrypted"].(bool) && ok {
|
||||
videoSources := make([]*hibikeonlinestream.VideoSource, 0)
|
||||
for _, e := range encryptedString.([]map[string]interface{}) {
|
||||
videoSources = append(videoSources, &hibikeonlinestream.VideoSource{
|
||||
URL: e["file"].(string),
|
||||
Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(e["file"].(string), ".m3u8")],
|
||||
Subtitles: subtitles,
|
||||
Quality: QualityAuto,
|
||||
})
|
||||
}
|
||||
if len(videoSources) == 0 {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
return videoSources, nil
|
||||
}
|
||||
case string:
|
||||
res, err = client.Get(m.Script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
text, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, errors.New("couldn't fetch script to decrypt resource")
|
||||
}
|
||||
|
||||
values, err := m.extractVariables(string(text))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret, encryptedSource := m.getSecret(encryptedString.(string), values)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
decrypted, err := m.decrypt(encryptedSource, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var decryptedData []map[string]interface{}
|
||||
err = json.Unmarshal([]byte(decrypted), &decryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sources := make([]*hibikeonlinestream.VideoSource, 0)
|
||||
for _, e := range decryptedData {
|
||||
sources = append(sources, &hibikeonlinestream.VideoSource{
|
||||
URL: e["file"].(string),
|
||||
Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(e["file"].(string), ".m3u8")],
|
||||
Subtitles: subtitles,
|
||||
Quality: QualityAuto,
|
||||
})
|
||||
}
|
||||
|
||||
if len(sources) == 0 {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
|
||||
func (m *MegaCloud) extractVariables(text string) ([][]int, error) {
|
||||
re := regexp.MustCompile(`case\s*0x[0-9a-f]+:\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);`)
|
||||
matches := re.FindAllStringSubmatch(text, -1)
|
||||
|
||||
var vars [][]int
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
caseLine := match[0]
|
||||
if strings.Contains(caseLine, "partKey") {
|
||||
continue
|
||||
}
|
||||
|
||||
matchKey1, err1 := m.matchingKey(match[1], text)
|
||||
matchKey2, err2 := m.matchingKey(match[2], text)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
key1, err1 := strconv.ParseInt(matchKey1, 16, 64)
|
||||
key2, err2 := strconv.ParseInt(matchKey2, 16, 64)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
vars = append(vars, []int{int(key1), int(key2)})
|
||||
}
|
||||
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
func (m *MegaCloud) matchingKey(value, script string) (string, error) {
|
||||
regexPattern := `,` + regexp.QuoteMeta(value) + `=((?:0x)?([0-9a-fA-F]+))`
|
||||
re := regexp.MustCompile(regexPattern)
|
||||
|
||||
match := re.FindStringSubmatch(script)
|
||||
if len(match) > 1 {
|
||||
return strings.TrimPrefix(match[1], "0x"), nil
|
||||
}
|
||||
|
||||
return "", errors.New("failed to match the key")
|
||||
}
|
||||
|
||||
func (m *MegaCloud) getSecret(encryptedString string, values [][]int) (string, string) {
|
||||
secret := ""
|
||||
encryptedSourceArray := strings.Split(encryptedString, "")
|
||||
currentIndex := 0
|
||||
|
||||
for _, index := range values {
|
||||
start := index[0] + currentIndex
|
||||
end := start + index[1]
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
secret += string(encryptedString[i])
|
||||
encryptedSourceArray[i] = ""
|
||||
}
|
||||
|
||||
currentIndex += index[1]
|
||||
}
|
||||
|
||||
encryptedSource := strings.Join(encryptedSourceArray, "")
|
||||
|
||||
return secret, encryptedSource
|
||||
}
|
||||
|
||||
//func (m *MegaCloud) getSecret(encryptedString string, values []int) (string, string, error) {
|
||||
// var secret string
|
||||
// var encryptedSource = encryptedString
|
||||
// var totalInc int
|
||||
//
|
||||
// for i := 0; i < values[0]; i++ {
|
||||
// var start, inc int
|
||||
//
|
||||
// switch i {
|
||||
// case 0:
|
||||
// start = values[2]
|
||||
// inc = values[1]
|
||||
// case 1:
|
||||
// start = values[4]
|
||||
// inc = values[3]
|
||||
// case 2:
|
||||
// start = values[6]
|
||||
// inc = values[5]
|
||||
// case 3:
|
||||
// start = values[8]
|
||||
// inc = values[7]
|
||||
// case 4:
|
||||
// start = values[10]
|
||||
// inc = values[9]
|
||||
// case 5:
|
||||
// start = values[12]
|
||||
// inc = values[11]
|
||||
// case 6:
|
||||
// start = values[14]
|
||||
// inc = values[13]
|
||||
// case 7:
|
||||
// start = values[16]
|
||||
// inc = values[15]
|
||||
// case 8:
|
||||
// start = values[18]
|
||||
// inc = values[17]
|
||||
// default:
|
||||
// return "", "", errors.New("invalid index")
|
||||
// }
|
||||
//
|
||||
// from := start + totalInc
|
||||
// to := from + inc
|
||||
//
|
||||
// secret += encryptedString[from:to]
|
||||
// encryptedSource = strings.Replace(encryptedSource, encryptedString[from:to], "", 1)
|
||||
// totalInc += inc
|
||||
// }
|
||||
//
|
||||
// return secret, encryptedSource, nil
|
||||
//}
|
||||
|
||||
func (m *MegaCloud) decrypt(encrypted, keyOrSecret string) (string, error) {
|
||||
cypher, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
salt := cypher[8:16]
|
||||
password := append([]byte(keyOrSecret), salt...)
|
||||
|
||||
md5Hashes := make([][]byte, 3)
|
||||
digest := password
|
||||
for i := 0; i < 3; i++ {
|
||||
hash := md5.Sum(digest)
|
||||
md5Hashes[i] = hash[:]
|
||||
digest = append(hash[:], password...)
|
||||
}
|
||||
|
||||
key := append(md5Hashes[0], md5Hashes[1]...)
|
||||
iv := md5Hashes[2]
|
||||
contents := cypher[16:]
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(contents, contents)
|
||||
|
||||
contents, err = pkcs7Unpad(contents, block.BlockSize())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(contents), nil
|
||||
}
|
||||
|
||||
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
if blockSize <= 0 {
|
||||
return nil, errors.New("invalid blocksize")
|
||||
}
|
||||
if len(data)%blockSize != 0 || len(data) == 0 {
|
||||
return nil, errors.New("invalid PKCS7 data (block size must be a multiple of input length)")
|
||||
}
|
||||
padLen := int(data[len(data)-1])
|
||||
if padLen > blockSize || padLen == 0 {
|
||||
return nil, errors.New("invalid PKCS7 padding")
|
||||
}
|
||||
for i := 0; i < padLen; i++ {
|
||||
if data[len(data)-1-i] != byte(padLen) {
|
||||
return nil, errors.New("invalid PKCS7 padding")
|
||||
}
|
||||
}
|
||||
return data[:len(data)-padLen], nil
|
||||
}
|
||||
111
seanime-2.9.10/internal/onlinestream/sources/streamsb.go
Normal file
111
seanime-2.9.10/internal/onlinestream/sources/streamsb.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package onlinestream_sources
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
type StreamSB struct {
|
||||
Host string
|
||||
Host2 string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
func NewStreamSB() *StreamSB {
|
||||
return &StreamSB{
|
||||
Host: "https://streamsss.net/sources50",
|
||||
Host2: "https://watchsb.com/sources50",
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StreamSB) Payload(hex string) string {
|
||||
return "566d337678566f743674494a7c7c" + hex + "7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362"
|
||||
}
|
||||
|
||||
func (s *StreamSB) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) {
|
||||
|
||||
defer util.HandlePanicInModuleThen("onlinestream/sources/streamsb/Extract", func() {
|
||||
err = ErrVideoSourceExtraction
|
||||
})
|
||||
|
||||
var ret []*hibikeonlinestream.VideoSource
|
||||
|
||||
id := strings.Split(uri, "/e/")[1]
|
||||
if strings.Contains(id, "html") {
|
||||
id = strings.Split(id, ".html")[0]
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return nil, errors.New("cannot find ID")
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/%s", s.Host, s.Payload(hex.EncodeToString([]byte(id)))), nil)
|
||||
req.Header.Add("watchsb", "sbstream")
|
||||
req.Header.Add("User-Agent", s.UserAgent)
|
||||
req.Header.Add("Referer", uri)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
var jsonResponse map[string]interface{}
|
||||
err = json.Unmarshal(body, &jsonResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamData, ok := jsonResponse["stream_data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, ErrNoVideoSourceFound
|
||||
}
|
||||
|
||||
m3u8Urls, err := client.Get(streamData["file"].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer m3u8Urls.Body.Close()
|
||||
|
||||
m3u8Body, err := io.ReadAll(m3u8Urls.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoList := strings.Split(string(m3u8Body), "#EXT-X-STREAM-INF:")
|
||||
|
||||
for _, video := range videoList {
|
||||
if !strings.Contains(video, "m3u8") {
|
||||
continue
|
||||
}
|
||||
|
||||
url := strings.Split(video, "\n")[1]
|
||||
quality := strings.Split(strings.Split(video, "RESOLUTION=")[1], ",")[0]
|
||||
quality = strings.Split(quality, "x")[1]
|
||||
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{
|
||||
URL: url,
|
||||
Quality: quality + "p",
|
||||
Type: hibikeonlinestream.VideoSourceM3U8,
|
||||
})
|
||||
}
|
||||
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{
|
||||
URL: streamData["file"].(string),
|
||||
Quality: "auto",
|
||||
Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(streamData["file"].(string), ".m3u8")],
|
||||
})
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
64
seanime-2.9.10/internal/onlinestream/sources/streamtape.go
Normal file
64
seanime-2.9.10/internal/onlinestream/sources/streamtape.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package onlinestream_sources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
)
|
||||
|
||||
type (
|
||||
Streamtape struct {
|
||||
Client *http.Client
|
||||
}
|
||||
)
|
||||
|
||||
func NewStreamtape() *Streamtape {
|
||||
return &Streamtape{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Streamtape) Extract(uri string) (vs []*hibikeonlinestream.VideoSource, err error) {
|
||||
defer util.HandlePanicInModuleThen("onlinestream/sources/streamtape/Extract", func() {
|
||||
err = ErrVideoSourceExtraction
|
||||
})
|
||||
|
||||
var ret []*hibikeonlinestream.VideoSource
|
||||
|
||||
resp, err := s.Client.Get(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`robotlink'\).innerHTML = (.*)'`)
|
||||
match := re.FindStringSubmatch(string(body))
|
||||
if len(match) == 0 {
|
||||
return nil, errors.New("could not find robotlink")
|
||||
}
|
||||
|
||||
fhsh := strings.Split(match[1], "+ ('")
|
||||
fh := fhsh[0]
|
||||
sh := fhsh[1][3:]
|
||||
|
||||
fh = strings.ReplaceAll(fh, "'", "")
|
||||
|
||||
url := "https:" + fh + sh
|
||||
|
||||
ret = append(ret, &hibikeonlinestream.VideoSource{
|
||||
URL: url,
|
||||
Type: map[bool]hibikeonlinestream.VideoSourceType{true: hibikeonlinestream.VideoSourceM3U8, false: hibikeonlinestream.VideoSourceMP4}[strings.Contains(url, ".m3u8")],
|
||||
Quality: QualityAuto,
|
||||
})
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
Reference in New Issue
Block a user