mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 07:46:37 +08:00
refactor: split modules and harden scraping workflows
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
)
|
||||
|
||||
// Item 单条目详情。
|
||||
func (e *EmbyService) Item(ctx context.Context, mediaID, userID string) (map[string]any, error) {
|
||||
if lib, err := e.repo.Library.FindByID(ctx, mediaID); err != nil {
|
||||
return nil, err
|
||||
} else if lib != nil {
|
||||
libs := FilterDisplayCloudLibraries(ctx, e.repo, []model.Library{*lib})
|
||||
if len(libs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
visibility := e.mediaVisibility(ctx, userID)
|
||||
if !e.libraryVisibleFromCachedVisibility(libs[0], visibility) {
|
||||
return nil, nil
|
||||
}
|
||||
return e.libraryAsView(&libs[0]), nil
|
||||
}
|
||||
if strings.HasPrefix(mediaID, embyVirtualSeasonPrefix) {
|
||||
if season, ok, err := e.findSeasonGroup(ctx, mediaID, userID); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return e.seasonPayload(season), nil
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(mediaID, embyVirtualSeriesPrefix) {
|
||||
if series, ok, err := e.findSeriesGroup(ctx, mediaID, userID); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return e.seriesPayload(series), nil
|
||||
}
|
||||
}
|
||||
m, err := e.repo.Media.FindByID(ctx, mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m == nil {
|
||||
if series, ok, err := e.findSeriesGroup(ctx, mediaID, userID); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return e.seriesPayload(series), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if !UserDefaultMediaVisibility(ctx, e.repo, userID).Allows(m) {
|
||||
return nil, nil
|
||||
}
|
||||
fav := false
|
||||
pos := int64(0)
|
||||
if userID != "" {
|
||||
var f model.Favorite
|
||||
ferr := e.repo.DB.WithContext(ctx).Where("user_id = ? AND media_id = ?", userID, mediaID).First(&f).Error
|
||||
if ferr == nil {
|
||||
fav = true
|
||||
}
|
||||
var h model.PlaybackHistory
|
||||
herr := e.repo.DB.WithContext(ctx).Where("user_id = ? AND media_id = ?", userID, mediaID).
|
||||
Order("watched_at desc").First(&h).Error
|
||||
if herr == nil {
|
||||
pos = h.PositionMs
|
||||
}
|
||||
}
|
||||
return e.itemPayload(ctx, m, fav, pos), nil
|
||||
}
|
||||
|
||||
// LatestItems 最近添加,全库或指定库。
|
||||
func (e *EmbyService) LatestItems(ctx context.Context, userID, parentID string, limit int) ([]map[string]any, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
cacheKey := e.embyLatestCacheKey(userID, parentID, limit)
|
||||
var cached embyLatestCacheValue
|
||||
if e.cache != nil && e.cache.GetJSON(ctx, cacheKey, &cached) {
|
||||
return cached.Items, nil
|
||||
}
|
||||
q := e.repo.DB.WithContext(ctx).Model(&model.Media{}).Where("deleted_at IS NULL")
|
||||
q = e.applyUserMediaVisibility(ctx, q, userID)
|
||||
if parentID != "" {
|
||||
if episodic, err := e.libraryIsEpisodic(ctx, parentID); err == nil && episodic {
|
||||
out, err := e.latestSeriesItemsForLibrary(ctx, userID, parentID, limit)
|
||||
if err == nil && e.cache != nil {
|
||||
e.cache.SetJSON(ctx, cacheKey, embyLatestCacheValue{Items: out}, time.Duration(e.mediaCacheTTLSeconds())*time.Second)
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
q = q.Where("library_id IN ?", e.mergedLibraryIDs(ctx, parentID))
|
||||
}
|
||||
var rows []model.Media
|
||||
if err := q.Order("media.created_at desc").Limit(limit).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
favs := map[string]bool{}
|
||||
if userID != "" && len(rows) > 0 {
|
||||
mediaIDs := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if strings.TrimSpace(row.ID) != "" {
|
||||
mediaIDs = append(mediaIDs, row.ID)
|
||||
}
|
||||
}
|
||||
if len(mediaIDs) == 0 {
|
||||
mediaIDs = []string{"__none__"}
|
||||
}
|
||||
var fr []model.Favorite
|
||||
_ = e.repo.DB.WithContext(ctx).Where("user_id = ? AND media_id IN ?", userID, mediaIDs).Find(&fr).Error
|
||||
for _, f := range fr {
|
||||
favs[f.MediaID] = true
|
||||
}
|
||||
}
|
||||
out := make([]map[string]any, 0, len(rows))
|
||||
for _, m := range rows {
|
||||
out = append(out, e.itemPayload(ctx, &m, favs[m.ID], 0))
|
||||
}
|
||||
if e.cache != nil {
|
||||
e.cache.SetJSON(ctx, cacheKey, embyLatestCacheValue{Items: out}, time.Duration(e.mediaCacheTTLSeconds())*time.Second)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (e *EmbyService) latestSeriesItemsForLibrary(ctx context.Context, userID, libraryID string, limit int) ([]map[string]any, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
rowLimit := limit * 40
|
||||
if rowLimit < 200 {
|
||||
rowLimit = 200
|
||||
}
|
||||
if rowLimit > embySeriesGroupingLimit {
|
||||
rowLimit = embySeriesGroupingLimit
|
||||
}
|
||||
q := e.repo.DB.WithContext(ctx).Model(&model.Media{}).
|
||||
Where("library_id IN ? AND (season_num > 0 OR episode_num > 0)", e.mergedLibraryIDs(ctx, libraryID))
|
||||
q = e.applyUserMediaVisibility(ctx, q, userID)
|
||||
var rows []model.Media
|
||||
if err := q.Order("media.created_at desc").Limit(rowLimit).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups := e.seriesGroupsFromMedia(rows)
|
||||
sortSeriesGroups(groups, ItemsParams{SortBy: "datecreated", SortOrder: "Descending"})
|
||||
if len(groups) > limit {
|
||||
groups = groups[:limit]
|
||||
}
|
||||
items := make([]map[string]any, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
items = append(items, e.seriesPayload(group))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ResumeItems 列出有未完成播放进度的媒体。
|
||||
func (e *EmbyService) ResumeItems(ctx context.Context, userID string, limit int) (map[string]any, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
var hist []model.PlaybackHistory
|
||||
if err := e.repo.DB.WithContext(ctx).
|
||||
Where("user_id = ? AND completed = ? AND position_ms > 0", userID, false).
|
||||
Order("watched_at desc").Limit(limit).Find(&hist).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hist) == 0 {
|
||||
return map[string]any{"Items": []any{}, "TotalRecordCount": 0}, nil
|
||||
}
|
||||
ids := make([]string, 0, len(hist))
|
||||
posByID := map[string]int64{}
|
||||
for _, h := range hist {
|
||||
ids = append(ids, h.MediaID)
|
||||
posByID[h.MediaID] = h.PositionMs
|
||||
}
|
||||
var medias []model.Media
|
||||
q := e.repo.DB.WithContext(ctx).Where("id IN ?", ids)
|
||||
q = e.applyUserMediaVisibility(ctx, q, userID)
|
||||
if err := q.Find(&medias).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID := map[string]*model.Media{}
|
||||
for i := range medias {
|
||||
byID[medias[i].ID] = &medias[i]
|
||||
}
|
||||
items := make([]map[string]any, 0, len(hist))
|
||||
for _, h := range hist {
|
||||
if m, ok := byID[h.MediaID]; ok {
|
||||
items = append(items, e.itemPayload(ctx, m, false, posByID[h.MediaID]))
|
||||
}
|
||||
}
|
||||
return map[string]any{"Items": items, "TotalRecordCount": len(items)}, nil
|
||||
}
|
||||
|
||||
func (e *EmbyService) itemPayload(ctx context.Context, m *model.Media, fav bool, posMs int64) map[string]any {
|
||||
itemType := "Movie"
|
||||
name := m.Title
|
||||
parentID := m.LibraryID
|
||||
seriesID := m.SeriesID
|
||||
seriesName := ""
|
||||
seasonID := ""
|
||||
if e.mediaShouldBeEpisode(ctx, m) {
|
||||
itemType = "Episode"
|
||||
seriesID = e.seriesIDForMedia(m)
|
||||
seriesName = e.seriesNameForMedia(m)
|
||||
seasonID = e.seasonIDForMedia(m)
|
||||
parentID = seasonID
|
||||
episodeTitle := strings.TrimSpace(m.EpisodeTitle)
|
||||
if episodeTitle != "" {
|
||||
name = episodeTitle
|
||||
} else if m.EpisodeNum > 0 {
|
||||
name = fmt.Sprintf("第 %d 集", m.EpisodeNum)
|
||||
}
|
||||
}
|
||||
imageTags := map[string]string{}
|
||||
backdropTags := []string{}
|
||||
primaryArtwork := e.mediaPrimaryArtwork(ctx, m)
|
||||
backdropArtwork := e.mediaBackdropArtwork(ctx, m)
|
||||
if primaryArtwork != "" {
|
||||
imageTags["Primary"] = m.ID
|
||||
}
|
||||
if backdropArtwork != "" {
|
||||
backdropTags = append(backdropTags, m.ID+"-bd")
|
||||
}
|
||||
|
||||
runTimeTicks := int64(m.DurationSec) * 10_000_000
|
||||
durationMs := int64(m.DurationSec) * 1000
|
||||
played := posMs > 0 && durationMs > 0 && posMs >= durationMs*9/10
|
||||
pct := 0.0
|
||||
if durationMs > 0 {
|
||||
pct = float64(posMs) / float64(durationMs) * 100
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"Id": m.ID,
|
||||
"Name": name,
|
||||
"OriginalTitle": m.OriginalName,
|
||||
"ServerId": embyServerID,
|
||||
"Type": itemType,
|
||||
"MediaType": "Video",
|
||||
"IsFolder": false,
|
||||
"ProductionYear": m.Year,
|
||||
"ParentIndexNumber": m.SeasonNum,
|
||||
"IndexNumber": m.EpisodeNum,
|
||||
"Overview": m.Overview,
|
||||
"RunTimeTicks": runTimeTicks,
|
||||
"CommunityRating": m.Rating,
|
||||
"Container": m.Container,
|
||||
"Width": m.Width,
|
||||
"Height": m.Height,
|
||||
"DateCreated": m.CreatedAt,
|
||||
"Path": m.Path,
|
||||
"ParentId": parentID,
|
||||
"SeasonId": seasonID,
|
||||
"SeasonName": seasonName(m.SeasonNum),
|
||||
"SeriesId": seriesID,
|
||||
"SeriesName": seriesName,
|
||||
"ImageTags": imageTags,
|
||||
"BackdropImageTags": backdropTags,
|
||||
"Genres": splitCSV(m.Genres),
|
||||
"ProviderIds": map[string]string{
|
||||
"Tmdb": intToStr(m.TMDbID),
|
||||
"Bangumi": intToStr(m.BangumiID),
|
||||
},
|
||||
"UserData": map[string]any{
|
||||
"PlaybackPositionTicks": posMs * 10_000,
|
||||
"PlayCount": 0,
|
||||
"IsFavorite": fav,
|
||||
"Played": played,
|
||||
"PlayedPercentage": pct,
|
||||
},
|
||||
"MediaSources": e.mediaSourcesForItem(ctx, m, true, false),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user