refactor: switch episode progress tracking database from Dexie.js to LocalStorage

This commit is contained in:
ThaUnknown 2024-01-13 18:41:30 +01:00 committed by michiel-cox
parent b140fbc03d
commit 35da5a2ca6
9 changed files with 60 additions and 70 deletions

View file

@ -2,9 +2,9 @@
import { statusColorMap } from '@/modules/anime.js'
import EpisodePreviewCard from './EpisodePreviewCard.svelte'
import { hoverClick } from '@/modules/click.js'
import { since } from '@/modules/util'
import { since } from '@/modules/util.js'
import { getContext } from 'svelte'
import { liveAnimeEpisodeProgress } from "@/modules/animeprogress";
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
export let data
let preview = false

View file

@ -1,7 +1,7 @@
<script>
import { statusColorMap, formatMap } from '@/modules/anime.js'
import { since } from '@/modules/util'
import { liveAnimeEpisodeProgress } from "@/modules/animeprogress";
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
export let data
const media = data.media

View file

@ -2,7 +2,7 @@
import { formatMap, setStatus, playMedia, getMediaMaxEp } from '@/modules/anime.js'
import { alRequest } from '@/modules/anilist.js'
import { click } from '@/modules/click.js'
import { liveAnimeEpisodeProgress } from "@/modules/animeprogress";
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
export let media
let hide = true
@ -84,7 +84,7 @@
src={`https://www.youtube-nocookie.com/embed/${media.trailer?.id}?autoplay=1&controls=0&mute=1&disablekb=1&loop=1&vq=medium&playlist=${media.trailer?.id}`}
/> -->
{#if progress && $progress > 0}
<div class="progress container-fluid position-absolute" style="margin-top: -1.5rem">
<div class=" position-absolute" style="margin-top: -1.5rem">
<div class="progress-bar" style="width: {$progress}%"></div>
</div>
{/if}

View file

@ -4,7 +4,7 @@
import { formatMap, statusColorMap, getMediaMaxEp } from '@/modules/anime.js'
import { hoverClick } from '@/modules/click.js'
import { countdown } from '@/modules/util.js'
import { liveAnimeEpisodeProgress } from "@/modules/animeprogress";
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
import { page } from '@/App.svelte'

View file

@ -1,59 +1,68 @@
import Dexie, { liveQuery } from 'dexie';
import { writable, derived } from 'simple-store-svelte'
export const db = new Dexie('miru');
// Maximum number of entries to keep in LocalStorage
const maxEntries = 1000
db.version(1).stores({
animeEpisodeProgress: '[mediaId+episode], currentTime, safeduration, createdAt, updatedAt', // mediaId and episode is the composite key
});
// LocalStorage is structured as an array of objects with the following properties:
// mediaId, episode, currentTime, safeduration, createdAt, updatedAt
function loadFromLocalStorage() {
const data = localStorage.getItem('animeEpisodeProgress')
return data ? JSON.parse(data) : []
}
function saveToLocalStorage(data) {
localStorage.setItem('animeEpisodeProgress', JSON.stringify(data))
animeProgressStore.set(data)
}
const animeProgressStore = writable(loadFromLocalStorage())
// Return an object with the progress of each episode in percent (0-100), keyed by episode number
export function liveAnimeProgress (mediaId){
return liveQuery(async () => {
if (!mediaId) return {}
const results = await db.animeEpisodeProgress.where({ mediaId }).toArray();
if (!results) return {}
// Return an object with the episode as the key and the progress as the value
return Object.fromEntries(results.map(result => [
result.episode,
Math.ceil(result.currentTime / result.safeduration * 100)
]))
});
return derived(animeProgressStore, (data) => {
if (!mediaId) return {}
const results = data.filter(item => item.mediaId === mediaId)
if (!results) return {}
// Return an object with the episode as the key and the progress as the value
return Object.fromEntries(results.map(result => [
result.episode,
Math.ceil(result.currentTime / result.safeduration * 100)
]))
})
}
// Return an individual episode's progress in percent (0-100)
export function liveAnimeEpisodeProgress (mediaId, episode) {
return liveQuery(async () => {
return derived(animeProgressStore, (data) => {
if (!mediaId || !episode) return 0
const result = await getAnimeProgress(mediaId, episode)
const result = data.find(item => item.mediaId === mediaId && item.episode === episode)
if (!result) return 0
return Math.ceil(result.currentTime / result.safeduration * 100)
});
})
}
// Return an individual episode's record { mediaId, episode, currentTime, safeduration, createdAt, updatedAt }
export function getAnimeProgress (mediaId, episode) {
return db.animeEpisodeProgress.where({ mediaId, episode }).first();
export function getAnimeProgress(mediaId, episode) {
const data = loadFromLocalStorage()
return data.find(item => item.mediaId === mediaId && item.episode === episode)
}
// Set an individual episode's progress
export async function setAnimeProgress ({mediaId, episode, currentTime, safeduration}) {
// If you want to update the record if it already exists:
const existing = await getAnimeProgress(mediaId, episode);
export function setAnimeProgress({ mediaId, episode, currentTime, safeduration }) {
const data = loadFromLocalStorage()
// Update the existing entry or create a new one
const existing = data.find(item => item.mediaId === mediaId && item.episode === episode)
if (existing) {
return await db.animeEpisodeProgress.update([ mediaId, episode ], {
currentTime,
safeduration,
updatedAt: Date.now(),
});
existing.currentTime = currentTime
existing.safeduration = safeduration
existing.updatedAt = Date.now()
} else {
data.push({ mediaId, episode, currentTime, safeduration, createdAt: Date.now(), updatedAt: Date.now() })
}
// If you want to insert a new record if it does not exist:
return await db.animeEpisodeProgress.put({
mediaId,
episode,
currentTime,
safeduration,
createdAt: Date.now(),
updatedAt: Date.now(),
});
// Remove the oldest entries if we have too many
while (data.length > maxEntries) {
const oldest = data.reduce((a, b) => a.updatedAt < b.updatedAt ? a : b)
data.splice(data.indexOf(oldest), 1)
}
saveToLocalStorage(data)
}

View file

@ -8,7 +8,6 @@
"anitomyscript": "github:ThaUnknown/anitomyscript#42290c4b3f256893be08a4e89051f448ff5e9d00",
"bottleneck": "^2.19.5",
"browser-event-target-emitter": "^1.0.1",
"dexie": "4.0.1-beta.6",
"jassub": "latest",
"js-levenshtein": "^1.1.6",
"p2pt": "github:ThaUnknown/p2pt#modernise",
@ -24,4 +23,4 @@
"video-deband": "^1.0.5",
"webpack-merge": "^5.10.0"
}
}
}

View file

@ -203,7 +203,7 @@
async function loadAnimeProgress () {
const mediaId = current?.media?.media?.id
const episode = current?.media?.episode
if (mediaId !== media?.media?.id || episode !== media?.episode) return
if (current?.media.failed || mediaId !== media?.media?.id || episode !== media?.episode) return
const animeProgress = await getAnimeProgress(mediaId, episode)
if (!animeProgress) return
const currentTime = Math.max(animeProgress.currentTime - 5, 0) // Load 5 seconds before
@ -212,8 +212,9 @@
function saveAnimeProgress () {
const mediaId = current?.media?.media?.id
const episode = current?.media.episode
if (mediaId !== media?.media?.id || episode !== media?.episode || buffering || paused || video.readyState < 4) return
const episode = current?.media?.episode
if (current?.media?.failed || mediaId !== media?.media?.id || episode !== media?.episode) return
if (buffering || paused || video.readyState < 4) return
setAnimeProgress({ mediaId, episode, currentTime: video.currentTime, safeduration })
}
setInterval(saveAnimeProgress, 1000)

View file

@ -2,8 +2,8 @@
import { since } from '@/modules/util'
import { click } from '@/modules/click.js'
import { getEpisodeNumberByAirDate } from '@/modules/providers/tosho.js'
import { alRequest } from '@/modules/anilist'
import { liveAnimeProgress } from "@/modules/animeprogress";
import { alRequest } from '@/modules/anilist.js'
import { liveAnimeProgress } from '@/modules/animeprogress.js'
export let media

View file

@ -151,9 +151,6 @@ importers:
browser-event-target-emitter:
specifier: ^1.0.1
version: 1.0.1
dexie:
specifier: 4.0.1-beta.6
version: 4.0.1-beta.6
jassub:
specifier: latest
version: 1.7.15
@ -2137,7 +2134,6 @@ packages:
/bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
requiresBuild: true
dependencies:
file-uri-to-path: 1.0.0
dev: true
@ -2324,7 +2320,6 @@ packages:
/boolean@3.2.0:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
requiresBuild: true
dev: true
optional: true
@ -3465,10 +3460,6 @@ packages:
resolution: {integrity: sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==}
dev: true
/dexie@4.0.1-beta.6:
resolution: {integrity: sha512-NOexH4Rn8mrdSg/bn3YNFdPzQ1vPNxgPYLGWGU46z26NYGW1XmC0hhjjttwx9jYwba1K9Ypo1ZbZLNKtK6INSg==}
dev: false
/diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
@ -3867,7 +3858,6 @@ packages:
/es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
requiresBuild: true
dev: true
optional: true
@ -4376,7 +4366,6 @@ packages:
/file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
requiresBuild: true
dev: true
optional: true
@ -5840,7 +5829,6 @@ packages:
/matcher@3.0.0:
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
engines: {node: '>=10'}
requiresBuild: true
dependencies:
escape-string-regexp: 4.0.0
dev: true
@ -6191,7 +6179,6 @@ packages:
/node-addon-api@1.7.2:
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
requiresBuild: true
dev: true
optional: true
@ -7281,7 +7268,6 @@ packages:
/roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
requiresBuild: true
dependencies:
boolean: 3.2.0
detect-node: 2.1.0
@ -7419,7 +7405,6 @@ packages:
/semver-compare@1.0.0:
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
requiresBuild: true
dev: true
optional: true
@ -7472,7 +7457,6 @@ packages:
/serialize-error@7.0.1:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
engines: {node: '>=10'}
requiresBuild: true
dependencies:
type-fest: 0.13.1
dev: true
@ -7825,7 +7809,6 @@ packages:
/sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
requiresBuild: true
dev: true
optional: true
@ -8486,7 +8469,6 @@ packages:
/type-fest@0.13.1:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
engines: {node: '>=10'}
requiresBuild: true
dev: true
optional: true
@ -8832,7 +8814,6 @@ packages:
/webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
requiresBuild: true
dev: false
optional: true