fix: render segment overrides and skip progress
This commit is contained in:
@@ -63,9 +63,10 @@ type SeasonEntry struct {
|
||||
}
|
||||
|
||||
type SkipSegment struct {
|
||||
Type string `json:"type"`
|
||||
Start float64 `json:"start"`
|
||||
End float64 `json:"end"`
|
||||
Type string `json:"type"`
|
||||
Start float64 `json:"start"`
|
||||
End float64 `json:"end"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderStream struct {
|
||||
|
||||
@@ -146,6 +146,11 @@ func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
|
||||
if u, ok := user.(*domain.User); ok {
|
||||
userID = u.ID
|
||||
}
|
||||
if userID == "" {
|
||||
// Avoid spamming 500s for anonymous playback; progress is user-scoped.
|
||||
c.Status(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MalID int64 `json:"mal_id"`
|
||||
|
||||
@@ -363,63 +363,50 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string,
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
|
||||
segments := []domain.SkipSegment{}
|
||||
|
||||
endpoint := fmt.Sprintf("https://api.aniskip.com/v1/skip-times/%s/%s?types=op&types=ed", url.PathEscape(strconv.Itoa(malID)), url.PathEscape(episode))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
req.Header.Set("User-Agent", useragent.Generic)
|
||||
if err == nil {
|
||||
req.Header.Set("User-Agent", useragent.Generic)
|
||||
if resp, err := s.httpClient.Do(req); err == nil {
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if body, err := io.ReadAll(io.LimitReader(resp.Body, limits.KiB512)); err == nil {
|
||||
type resultItem struct {
|
||||
SkipType string `json:"skip_type"`
|
||||
Interval struct {
|
||||
StartTime float64 `json:"start_time"`
|
||||
EndTime float64 `json:"end_time"`
|
||||
} `json:"interval"`
|
||||
}
|
||||
type apiResponse struct {
|
||||
Found bool `json:"found"`
|
||||
Result []resultItem `json:"results"`
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, limits.KiB512))
|
||||
if err != nil {
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
|
||||
type resultItem struct {
|
||||
SkipType string `json:"skip_type"`
|
||||
Interval struct {
|
||||
StartTime float64 `json:"start_time"`
|
||||
EndTime float64 `json:"end_time"`
|
||||
} `json:"interval"`
|
||||
}
|
||||
type apiResponse struct {
|
||||
Found bool `json:"found"`
|
||||
Result []resultItem `json:"results"`
|
||||
}
|
||||
|
||||
var parsed apiResponse
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
|
||||
if !parsed.Found || len(parsed.Result) == 0 {
|
||||
return []domain.SkipSegment{}
|
||||
}
|
||||
|
||||
segments := make([]domain.SkipSegment, 0, len(parsed.Result))
|
||||
for _, r := range parsed.Result {
|
||||
skipType := strings.ToLower(r.SkipType)
|
||||
switch skipType {
|
||||
case "op":
|
||||
skipType = "opening"
|
||||
case "ed":
|
||||
skipType = "ending"
|
||||
var parsed apiResponse
|
||||
if err := json.Unmarshal(body, &parsed); err == nil && parsed.Found && len(parsed.Result) > 0 {
|
||||
segments = make([]domain.SkipSegment, 0, len(parsed.Result))
|
||||
for _, r := range parsed.Result {
|
||||
skipType := strings.ToLower(r.SkipType)
|
||||
switch skipType {
|
||||
case "op":
|
||||
skipType = "opening"
|
||||
case "ed":
|
||||
skipType = "ending"
|
||||
}
|
||||
segments = append(segments, domain.SkipSegment{
|
||||
Type: skipType,
|
||||
Start: r.Interval.StartTime,
|
||||
End: r.Interval.EndTime,
|
||||
Source: "aniskip",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
segments = append(segments, domain.SkipSegment{
|
||||
Type: skipType,
|
||||
Start: r.Interval.StartTime,
|
||||
End: r.Interval.EndTime,
|
||||
})
|
||||
}
|
||||
|
||||
epNum, _ := strconv.ParseInt(strings.TrimSpace(episode), 10, 64)
|
||||
@@ -438,7 +425,12 @@ func (s *playbackService) fetchSkipSegments(ctx context.Context, userID string,
|
||||
default:
|
||||
continue
|
||||
}
|
||||
overrideByType[t] = domain.SkipSegment{Type: t, Start: o.StartTime, End: o.EndTime}
|
||||
overrideByType[t] = domain.SkipSegment{
|
||||
Type: t,
|
||||
Start: o.StartTime,
|
||||
End: o.EndTime,
|
||||
Source: "override",
|
||||
}
|
||||
}
|
||||
if len(overrideByType) > 0 {
|
||||
merged := make([]domain.SkipSegment, 0, len(segments)+len(overrideByType))
|
||||
|
||||
@@ -22,6 +22,8 @@ const sendBeacon = (payload: string) => {
|
||||
*/
|
||||
export const saveProgress = async (): Promise<void> => {
|
||||
if (state.transitionEpisode !== null || !state.malID || state.video.currentTime < 1) return;
|
||||
// progress is user-scoped; avoid spamming 401s for anonymous sessions
|
||||
if (!document.cookie.includes('mal_session=')) return;
|
||||
const episode = Number.parseInt(state.currentEpisode, 10);
|
||||
if (!episode) return;
|
||||
|
||||
@@ -60,6 +62,8 @@ const scheduleProgressSave = (): void => {
|
||||
*/
|
||||
export const markEpisodeTransition = (episodeNumber: number): void => {
|
||||
if (!state.malID || !episodeNumber) return;
|
||||
// progress is user-scoped; avoid sending beacons for anonymous sessions
|
||||
if (!document.cookie.includes('mal_session=')) return;
|
||||
if (state.progressSaveTimer !== undefined) {
|
||||
window.clearTimeout(state.progressSaveTimer);
|
||||
state.progressSaveTimer = undefined;
|
||||
@@ -102,6 +106,7 @@ export const setupProgress = (): void => {
|
||||
// save on page close
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (state.transitionEpisode !== null || state.completionSent || !state.malID) return;
|
||||
if (!document.cookie.includes('mal_session=')) return;
|
||||
const episode = Number.parseInt(state.currentEpisode, 10);
|
||||
if (!episode) return;
|
||||
sendBeacon(buildPayload(episode, displayTimeFromAbsolute(state.video.currentTime)));
|
||||
|
||||
@@ -151,10 +151,16 @@ export const setupSegmentEditor = (): void => {
|
||||
if (normalizedType === 'ending') return t !== 'ed' && t !== 'ending' && t !== 'outro';
|
||||
return t !== 'op' && t !== 'opening' && t !== 'intro';
|
||||
});
|
||||
state.parsedSegments.push({ type: normalizedType, start: startTime, end: endTime });
|
||||
state.parsedSegments.push({
|
||||
type: normalizedType,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
source: 'override',
|
||||
});
|
||||
resolveActiveSegments();
|
||||
renderSegments();
|
||||
|
||||
window.showToast?.({ message: 'Segment saved.' });
|
||||
close();
|
||||
} catch {
|
||||
setError('Failed to save segment.');
|
||||
|
||||
@@ -27,6 +27,7 @@ export const resolveActiveSegments = (): void => {
|
||||
state.activeSegments = state.parsedSegments.filter(s => {
|
||||
const t = normalizeType(s.type);
|
||||
if (!t) return false;
|
||||
const isOverride = (s.source || '').toLowerCase() === 'override';
|
||||
|
||||
const len = s.end - s.start;
|
||||
// duration filter
|
||||
@@ -34,6 +35,9 @@ export const resolveActiveSegments = (): void => {
|
||||
// bounds check
|
||||
if (s.start < 0 || s.end <= s.start || s.end > bounds + 1) return false;
|
||||
|
||||
// User overrides should render even if they don't fit AniSkip's usual OP/ED heuristics.
|
||||
if (isOverride) return true;
|
||||
|
||||
// intro: starts early, before 50% of video
|
||||
if (t === 'op') {
|
||||
return s.start <= MAX_INTRO_START && s.start <= bounds * 0.5;
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface SkipSegment {
|
||||
type: string; // 'op' or 'ed'
|
||||
start: number;
|
||||
end: number;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
// parsed subtitle cue from VTT
|
||||
@@ -37,6 +38,7 @@ export interface ActiveSegment {
|
||||
type: string;
|
||||
start: number;
|
||||
end: number;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
// timeline range (handles seekable ranges in live streams)
|
||||
|
||||
Reference in New Issue
Block a user