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