Fix episode attributes (#34)

This commit is contained in:
Ollie Taylor
2023-02-25 18:35:50 +00:00
committed by GitHub
24 changed files with 158 additions and 227 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'svelte-podcast': minor
---
Improvements to audio stores:
- Adds missing `is_paused` value.
- Fixes to `start_at` value
- Refactors localStorage stores & user preferences
+5 -4
View File
@@ -37,7 +37,8 @@
"svelte": "^3.54.0"
},
"dependencies": {
"just-clamp": "^4.2.0"
"just-clamp": "^4.2.0",
"svelte-local-storage-store": "^0.4.0"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
@@ -47,14 +48,14 @@
"@sveltejs/package": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"eslint": "^8.28.0",
"prettier-plugin-svelte": "^2.8.1",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"publint": "^0.1.9",
"svelte-check": "^3.0.1",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
+13 -6
View File
@@ -1,18 +1,27 @@
import { browser } from '$app/environment';
import { user_preferences } from '$lib/user';
import { onMount } from 'svelte';
import { readable } from 'svelte/store';
import { get, readable } from 'svelte/store';
export const audio_element = readable<HTMLAudioElement | null>(null, (set) => {
if (!browser) return;
const ID = 'svelte-podcast-generated-audio-element';
onMount(() => {
const el = document.createElement('audio');
const existing_element = document.getElementById(ID) as HTMLAudioElement | null;
const el = existing_element || document.createElement('audio');
el.id = ID;
el.setAttribute('preload', 'metadata');
el.muted = false;
el.autoplay = true;
el.controls = true;
document.body.appendChild(el);
el.controls = false;
const preferences = get(user_preferences);
el.playbackRate = preferences.playback_rate;
el.volume = preferences.volume;
if (!existing_element) document.body.appendChild(el);
set(el);
});
@@ -21,5 +30,3 @@ export const audio_element = readable<HTMLAudioElement | null>(null, (set) => {
element ? element.remove() : null;
};
});
export type AudioElementStore = typeof audio_element;
+16 -5
View File
@@ -1,13 +1,24 @@
<script lang="ts">
import { audio_element } from '$lib/audio/audio-element';
import { load_podcast_state } from '$lib/utility';
import { info } from '$lib/utility/package/log';
import { user_preferences } from '$lib/user';
import { info, warn } from '$lib/utility/package/log';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
$: $audio_element;
/* onMount(() => {
return audio_element.subscribe((el) => {
if (el) info('mounted audio element :: ', el);
});
}); */
onMount(() => {
load_podcast_state();
return audio_element.subscribe((el) => {
info('loaded audio :: ', el);
return user_preferences.subscribe((prefs) => {
const el = get(audio_element);
if (!el) return warn('no audio element found');
info('setting preferences :: ', prefs);
el.volume = prefs.volume;
el.playbackRate = prefs.playback_rate;
});
});
</script>
+30 -18
View File
@@ -1,24 +1,22 @@
import { audio_element, type AudioElementStore } from '$lib/audio/audio-element';
import { episode_details, type EpisodeDetailsStore } from '$lib/audio/episode-details';
import { user_preferences } from '$lib/preferences';
import { podcast_progress } from '$lib/progress';
import { audio_element } from '$lib/audio/audio-element';
import { episode_details } from '$lib/audio/episode-details';
import type { EpisodeAttributes, EpisodeDetails } from '$lib/types';
import { user_preferences, user_progress } from '$lib/user';
import { warn } from '$lib/utility/package/log';
import clamp from 'just-clamp';
import { derived, get } from 'svelte/store';
import { derived, get, type Readable } from 'svelte/store';
const default_episode_attributes = {
will_autoplay: false,
is_muted: false,
is_paused: true,
duration: 0,
start_at: 0,
details: null,
} satisfies Omit<EpisodeAttributes, 'src'>;
const episode_attributes = derived<
[AudioElementStore, EpisodeDetailsStore],
EpisodeAttributes | null
>([audio_element, episode_details], ([$audio, $details], set) => {
type EpisodeAttributesStore = Readable<EpisodeAttributes | null>;
const episode_attributes = derived([audio_element, episode_details], ([$audio, $details], set) => {
if (!$audio) return set(null);
function set_value() {
@@ -28,12 +26,22 @@ const episode_attributes = derived<
src: $audio.src,
duration: $audio.duration,
details: $details,
will_autoplay: $audio.autoplay,
is_paused: $audio.paused,
start_at: user_progress.get($audio.src) ?? 0,
});
}
$audio.addEventListener('loadeddata', () => set_value());
return () => $audio.removeEventListener('loadeddata', () => set_value());
});
$audio.addEventListener('loadeddata', set_value);
$audio.addEventListener('pause', set_value);
$audio.addEventListener('playing', set_value);
return () => {
$audio.removeEventListener('loadeddata', set_value);
$audio.removeEventListener('pause', set_value);
$audio.removeEventListener('playing', set_value);
};
}) satisfies EpisodeAttributesStore;
type HandleType = 'toggle' | 'set';
@@ -42,13 +50,17 @@ const no_element = (action: string) => warn(`could not ${action} :: no audio ele
export const episode_audio = {
subscribe: episode_attributes.subscribe,
load: (src: string, details: EpisodeDetails) => {
podcast_progress.stash_episode();
user_progress.save();
const el = get(audio_element);
if (!el) return no_element('load');
const progress = podcast_progress.get_episode(src);
el.src = src;
el.currentTime = progress?.start_at || 0;
el.muted = false;
// using user_progress
const start_at = user_progress.get(src) || 0;
el.currentTime = start_at;
// using user_preferences
const preferences = get(user_preferences);
el.playbackRate = preferences.playback_rate;
el.volume = preferences.volume;
@@ -56,7 +68,7 @@ export const episode_audio = {
episode_details.set(details);
},
unload: () => {
podcast_progress.stash_episode();
user_progress.save();
const el = get(audio_element);
if (!el) return no_element('unload');
el.src = '';
@@ -73,7 +85,7 @@ export const episode_audio = {
}
},
pause: (t?: HandleType) => {
podcast_progress.stash_episode();
user_progress.save();
const el = get(audio_element);
if (!el) return no_element('pause');
-1
View File
@@ -2,4 +2,3 @@ import type { EpisodeDetails } from '$lib/types';
import { writable } from 'svelte/store';
export const episode_details = writable<EpisodeDetails | null>(null);
export type EpisodeDetailsStore = typeof episode_details;
+17 -18
View File
@@ -1,7 +1,7 @@
import { audio_element, type AudioElementStore } from '$lib/audio/audio-element';
import { audio_element } from '$lib/audio/audio-element';
import type { EpisodeProgress } from '$lib/types';
import { secondsToTimestamp } from '$lib/utility';
import { derived } from 'svelte/store';
import { derived, type Readable } from 'svelte/store';
const default_episode_progress = {
current_time: 0,
@@ -9,21 +9,20 @@ const default_episode_progress = {
has_ended: false,
} satisfies EpisodeProgress;
export const episode_progress = derived<AudioElementStore, EpisodeProgress>(
audio_element,
($audio, set) => {
if (!$audio) return set(default_episode_progress);
type ProgressStore = Readable<EpisodeProgress>;
function set_value() {
if (!$audio) return;
set({
current_time: $audio.currentTime,
timestamp: secondsToTimestamp($audio.currentTime),
has_ended: $audio.ended,
});
}
export const episode_progress = derived(audio_element, ($audio, set) => {
if (!$audio) return set(default_episode_progress);
$audio.addEventListener('timeupdate', () => set_value());
return () => $audio.removeEventListener('timeupdate', () => set_value());
},
);
function set_value() {
if (!$audio) return;
set({
current_time: $audio.currentTime,
timestamp: secondsToTimestamp($audio.currentTime),
has_ended: $audio.ended,
});
}
$audio.addEventListener('timeupdate', () => set_value());
return () => $audio.removeEventListener('timeupdate', () => set_value());
}) satisfies ProgressStore;
+1 -2
View File
@@ -1,4 +1,3 @@
export * from '$lib/audio';
export * from '$lib/preferences';
export * from '$lib/progress';
export * from '$lib/user';
export * from '$lib/utility';
-37
View File
@@ -1,37 +0,0 @@
import { browser } from '$app/environment';
import { user_preferences } from '$lib/preferences/db-state';
import type { UserPreferences } from '$lib/types';
import { info, warn } from '$lib/utility/package/log';
import { get } from 'svelte/store';
const USER_PREFERENCE_KEY = 'USER_PREFERENCE' as const;
export function save_preferences() {
if (!browser || !localStorage) {
warn('localStorage not available, skipping save');
return;
}
const preferences = get(user_preferences);
info(`Saving user preferences to localStorage`, preferences);
localStorage.setItem(USER_PREFERENCE_KEY, JSON.stringify(preferences));
}
export function load_preferences() {
if (!browser || !localStorage) {
warn('localStorage not available, skipping load');
return;
}
const data = localStorage.getItem(USER_PREFERENCE_KEY);
if (!data) {
info('No saved user preferences found');
return;
}
const preferences = JSON.parse(data) as UserPreferences;
user_preferences.edit(preferences);
info(`Loaded user preferences from localStorage`, preferences);
}
-29
View File
@@ -1,29 +0,0 @@
import type { UserPreferences } from '$lib/types';
import clamp from 'just-clamp';
import { writable } from 'svelte/store';
import { load_preferences, save_preferences } from './db-local-storage';
const _default_user_preferences = { playback_rate: 1, volume: 1 } satisfies UserPreferences;
const _user_preferences = writable<UserPreferences>(_default_user_preferences);
const clamp_preferences = (prefs: UserPreferences) => ({
playback_rate: clamp(prefs.playback_rate, 0.5, 5),
volume: clamp(prefs.volume, 0, 1),
});
const edit = (prefs: Partial<UserPreferences>) => {
_user_preferences.update((prev) => clamp_preferences({ ...prev, ...prefs }));
save_preferences();
};
const clear = () => {
_user_preferences.set(_default_user_preferences);
save_preferences();
};
export const user_preferences = {
subscribe: _user_preferences.subscribe,
edit,
clear,
load: load_preferences,
};
-1
View File
@@ -1 +0,0 @@
export * from './db-state';
-1
View File
@@ -1 +0,0 @@
export const _episode_progress_map = new Map<string, number>();
-48
View File
@@ -1,48 +0,0 @@
import { browser } from '$app/environment';
import { _episode_progress_map } from '$lib/progress/_private';
import { error, info, warn } from '$lib/utility/package/log';
type SavedEpisodeProgress = { src: string; current_time: number };
const EPISODE_PROGRESS_KEY = 'EPISODE_PROGRESS' as const;
export function save_podcast_progress() {
if (!browser || !localStorage) {
warn('localStorage not available, skipping save');
return;
}
const items = [..._episode_progress_map];
console.log('items', items);
const episodes = items.reduce((prev, [src, current_time]) => {
return [...prev, { src, current_time }];
}, [] as SavedEpisodeProgress[]);
info(`Saving progress for ${episodes.length} episodes to localStorage`, episodes);
localStorage.setItem(EPISODE_PROGRESS_KEY, JSON.stringify(episodes));
}
export function load_podcast_progress() {
if (!browser || !localStorage) {
warn('localStorage not available, skipping load');
return;
}
const data = localStorage.getItem(EPISODE_PROGRESS_KEY);
if (!data) {
info('No saved episode progress found');
return;
}
const episodes = JSON.parse(data) as SavedEpisodeProgress[];
try {
episodes.forEach(({ src, current_time }) => {
_episode_progress_map.set(src, current_time);
});
info(`Loaded ${_episode_progress_map.size} episodes progress from localStorage`);
} catch (e) {
error('Error loading episode progress', e);
return;
}
}
-43
View File
@@ -1,43 +0,0 @@
import { episode_audio, episode_progress } from '$lib/audio';
import { _episode_progress_map } from '$lib/progress/_private';
import type { AudioLoadOptions } from '$lib/types';
import { info } from '$lib/utility/package/log';
import { get } from 'svelte/store';
import { load_podcast_progress, save_podcast_progress } from './db-local-storage';
const get_src_pathname = (src: string) => {
if (src.startsWith('http')) return new URL(src).pathname;
else return new URL(src, 'https://svelte.dev').pathname;
};
const stash_episode_progress = () => {
const audio = get(episode_audio);
console.log('audio', audio);
if (!audio?.src) return;
info('saving progress: ', audio.src);
const pathname = get_src_pathname(audio.src);
_episode_progress_map.set(pathname, get(episode_progress).current_time);
save_podcast_progress();
};
const get_episode_progress = (src: string): Pick<AudioLoadOptions, 'start_at'> | null => {
const start_at = _episode_progress_map.get(src);
if (!start_at) return null;
info('found saved progress: ', src, start_at);
return { start_at: start_at };
};
const clear = () => {
_episode_progress_map.clear();
save_podcast_progress();
};
export const podcast_progress = {
stash_episode: stash_episode_progress,
get_episode: get_episode_progress,
load: load_podcast_progress,
clear,
};
-1
View File
@@ -1 +0,0 @@
export * from './db-state';
+5 -1
View File
@@ -6,7 +6,7 @@ export type EpisodeProgress = { current_time: number; timestamp: string; has_end
export type EpisodeAttributes = {
will_autoplay: boolean;
is_muted: boolean;
is_paused: boolean;
duration: number;
src: string;
start_at: number;
@@ -22,3 +22,7 @@ export interface UserPreferences {
playback_rate: number;
volume: number;
}
export type UserProgress = {
[key: string]: number;
};
+2
View File
@@ -0,0 +1,2 @@
export * from './preferences';
export * from './progress';
+21
View File
@@ -0,0 +1,21 @@
import type { UserPreferences } from '$lib/types';
import clamp from 'just-clamp';
import { persisted } from 'svelte-local-storage-store';
const _default_user_preferences = { playback_rate: 1, volume: 1 } satisfies UserPreferences;
const _user_preferences = persisted<UserPreferences>('USER_PREFERENCE', _default_user_preferences);
export const user_preferences = {
subscribe: _user_preferences.subscribe,
set: {
playback_rate: (value: number) => {
const playback_rate = clamp(value, 0.5, 5);
return _user_preferences.update((prefs) => ({ ...prefs, playback_rate }));
},
volume: (value: number) => {
const volume = clamp(value, 0, 1);
return _user_preferences.update((prefs) => ({ ...prefs, volume }));
},
},
clear: () => _user_preferences.set(_default_user_preferences),
};
+27
View File
@@ -0,0 +1,27 @@
import { episode_audio, episode_progress } from '$lib/audio';
import type { UserProgress } from '$lib/types';
import { get_pathname_from_url } from '$lib/utility/get-pathname-from-url';
import { info } from '$lib/utility/package/log';
import { persisted } from 'svelte-local-storage-store';
import { get } from 'svelte/store';
const _default_user_progress = {} satisfies UserProgress;
const _user_progress = persisted<UserProgress>('USER_PROGRESS', _default_user_progress);
const save = () => {
const audio = get(episode_audio);
if (!audio?.src) return;
info('saving progress: ', audio.src);
const pathname = get_pathname_from_url(audio.src);
const current_time = get(episode_progress).current_time;
_user_progress.update((prev) => ({ ...prev, [pathname]: current_time }));
};
export const user_progress = {
subscribe: _user_progress.subscribe,
get: (src: string) => get(_user_progress)[get_pathname_from_url(src)],
save,
clear: () => _user_progress.set(_default_user_progress),
};
+4
View File
@@ -0,0 +1,4 @@
export const get_pathname_from_url = (src: string) => {
if (src.startsWith('http')) return new URL(src).pathname;
else return new URL(src, 'https://svelte.dev').pathname;
};
-1
View File
@@ -1,2 +1 @@
export * from './seconds-to-timestamp';
export * from './use-state';
-7
View File
@@ -1,7 +0,0 @@
import { user_preferences } from '$lib/preferences';
import { podcast_progress } from '$lib/progress';
export function load_podcast_state() {
podcast_progress.load();
user_preferences.load();
}
+3 -4
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { podcast_progress, user_preferences } from '$lib';
import { episode_audio, episode_progress } from '$lib/audio';
import { episode_audio, episode_progress, user_preferences, user_progress } from '$lib';
const sources = {
syntax: {
@@ -31,7 +30,7 @@
<h1>Demo</h1>
<a href="/another-page">Another Page</a>
<button type="button" on:click={podcast_progress.clear}>Clear progress for all episodes</button>
<button type="button" on:click={user_progress.clear}>Clear progress for all episodes</button>
<button type="button" on:click={user_preferences.clear}>Clear all preferences</button>
<h5>Load Audio</h5>
@@ -77,7 +76,7 @@
<h6>Playback Rate</h6>
{#each [0.5, 1, 2, 3] as rate}
<button type="button" on:click={() => user_preferences.edit({ playback_rate: rate })}>
<button type="button" on:click={() => user_preferences.set.playback_rate(rate)}>
{rate}x
</button>
{/each}
+5
View File
@@ -2799,6 +2799,11 @@ svelte-hmr@^0.15.1:
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.15.1.tgz#d11d878a0bbb12ec1cba030f580cd2049f4ec86b"
integrity sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==
svelte-local-storage-store@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/svelte-local-storage-store/-/svelte-local-storage-store-0.4.0.tgz#965e342582555b606102045e86662d6c5864e1e6"
integrity sha512-ctPykTt4S3BE5bF0mfV0jKiUR1qlmqLvnAkQvYHLeb9wRyO1MdIFDVI23X+TZEFleATHkTaOpYZswIvf3b2tWA==
svelte-preprocess@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.0.1.tgz#3dd21a17eb508347d4b26a0d98059d23e2d1b9a0"