Watch more seasons of this anime
@@ -201,6 +207,11 @@ templ VideoPlayer(data WatchPageData) {
data-video-player
data-stream-url="/watch/proxy/stream"
data-current-episode={ data.CurrentEpisode }
+ data-anime-title={ data.Title }
+ data-anime-title-english={ data.TitleEnglish }
+ data-anime-title-japanese={ data.TitleJapanese }
+ data-anime-image={ data.ImageURL }
+ data-anime-airing={ fmt.Sprintf("%v", data.Airing) }
data-start-time-seconds={ fmt.Sprintf("%.3f", data.StartTimeSeconds) }
data-initial-mode={ data.InitialMode }
data-available-modes={ toJSON(data.AvailableModes) }
diff --git a/internal/templates/watchlist.templ b/internal/templates/watchlist.templ
index 408d50a..549521c 100644
--- a/internal/templates/watchlist.templ
+++ b/internal/templates/watchlist.templ
@@ -107,7 +107,7 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
}
templ ifHasProgress(entry database.GetUserWatchListRow) {
- if entry.CurrentEpisode.Valid && entry.CurrentEpisode.Int64 > 0 {
+ if entry.CurrentEpisode.Valid && entry.CurrentEpisode.Int64 > 0 && entry.Status != "completed" {
Continue ep { fmt.Sprintf("%d", entry.CurrentEpisode.Int64) }
if entry.CurrentTimeSeconds > 0 {
diff --git a/static/player.ts b/static/player.ts
index 2ac9343..74de089 100644
--- a/static/player.ts
+++ b/static/player.ts
@@ -49,6 +49,11 @@ const initPlayer = (): void => {
const currentEpisode = container.getAttribute('data-current-episode') || '1'
const malID = Number.parseInt(container.getAttribute('data-mal-id') || '', 10)
const totalEpisodes = Number.parseInt(container.getAttribute('data-total-episodes') || '0', 10)
+ const animeTitle = container.getAttribute('data-anime-title') || ''
+ const animeTitleEnglish = container.getAttribute('data-anime-title-english') || ''
+ const animeTitleJapanese = container.getAttribute('data-anime-title-japanese') || ''
+ const animeImage = container.getAttribute('data-anime-image') || ''
+ const animeAiring = (container.getAttribute('data-anime-airing') || '').toLowerCase() === 'true'
const startTimeSeconds = Number.parseFloat(container.getAttribute('data-start-time-seconds') || '0')
const modeSources = JSON.parse(container.getAttribute('data-mode-sources') || '{}')
const availableModes = JSON.parse(container.getAttribute('data-available-modes') || '[]')
@@ -86,6 +91,8 @@ const initPlayer = (): void => {
let lastSavedProgress = { episode: currentEpisode, seconds: -1 }
let progressSaveTimer: number | undefined
let transitionEpisode: number | null = null
+ let completionSent = false
+ let completionAttempts = 0
const previewPopover = container.querySelector('[data-preview-popover]') as HTMLElement
const previewTime = container.querySelector('[data-preview-time]') as HTMLElement
@@ -617,21 +624,75 @@ const initPlayer = (): void => {
}
const completeAnime = async (episodeNumber: number): Promise => {
+ if (completionSent) return
if (!Number.isInteger(malID) || malID <= 0) return
if (!Number.isInteger(episodeNumber) || episodeNumber <= 0) return
+ completionSent = true
+
try {
- await fetch('/api/watch-complete', {
+ const response = await fetch('/api/watch-complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
+ keepalive: true,
body: JSON.stringify({
mal_id: malID,
episode: episodeNumber,
}),
})
+
+ if (!response.ok) {
+ completionSent = false
+ if (completionAttempts < 2) {
+ completionAttempts += 1
+ window.setTimeout(() => {
+ completeAnime(episodeNumber)
+ }, 1000)
+ }
+ return
+ }
+
+ const dropdownTrigger = document.querySelector('[data-dropdown-trigger]') as HTMLButtonElement | null
+ if (dropdownTrigger) {
+ dropdownTrigger.innerHTML = 'Completed ▾'
+ }
+
+ const watchStatusDropdown = document.getElementById('watch-status-dropdown')
+ if (watchStatusDropdown) {
+ const payload = {
+ anime_id: String(malID),
+ anime_title: animeTitle,
+ anime_title_english: animeTitleEnglish,
+ anime_title_japanese: animeTitleJapanese,
+ anime_image: animeImage,
+ status: 'completed',
+ airing: animeAiring,
+ }
+
+ fetch('/api/watchlist', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'HX-Request': 'true',
+ },
+ body: `anime_id=${encodeURIComponent(payload.anime_id)}&anime_title=${encodeURIComponent(payload.anime_title)}&anime_title_english=${encodeURIComponent(payload.anime_title_english)}&anime_title_japanese=${encodeURIComponent(payload.anime_title_japanese)}&anime_image=${encodeURIComponent(payload.anime_image)}&status=${encodeURIComponent(payload.status)}&airing=${encodeURIComponent(String(payload.airing))}`,
+ credentials: 'same-origin',
+ }).then(async (res) => {
+ if (!res.ok) return
+ const html = await res.text()
+ watchStatusDropdown.outerHTML = `${html}`
+ }).catch(() => {})
+ }
} catch {
+ completionSent = false
+ if (completionAttempts < 2) {
+ completionAttempts += 1
+ window.setTimeout(() => {
+ completeAnime(episodeNumber)
+ }, 1000)
+ }
return
}
}
@@ -830,6 +891,7 @@ const initPlayer = (): void => {
window.addEventListener('beforeunload', () => {
if (transitionEpisode !== null) return
+ if (completionSent) return
if (!Number.isInteger(malID) || malID <= 0) return
if (!video.duration || !Number.isFinite(video.duration)) return
const episodeNumber = Number.parseInt(currentEpisode, 10)