Skip to content

Commit aec884f

Browse files
author
Garrett Downs
committed
feat: add support for playing playlists
1 parent 422a01e commit aec884f

8 files changed

Lines changed: 163 additions & 48 deletions

File tree

src/components/AudioPlayer.svelte

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,90 @@
11
<script context="module" lang="ts">
22
import throttle from 'lodash.throttle';
3-
import { SoundCloud } from '../lib/soundcloud';
4-
import type { PlaybackProgress } from '../models/PlaybackProgress';
53
import { player } from '../stores/player';
64
import { clamp } from '../utils/clamp';
75
86
let audio = new Audio();
97
(audio as any).mozAudioChannelType = 'content';
108
119
audio.onloadedmetadata = () => {
12-
updateStatus({ duration: audio.duration });
10+
player.update({ duration: audio.duration });
1311
};
1412
1513
audio.ontimeupdate = throttle(() => {
16-
updateStatus({ currentTime: audio.currentTime });
14+
player.update({ currentTime: audio.currentTime });
1715
}, 1000);
1816
19-
export async function load(trackId: number) {
20-
const [track, streamUrl] = await Promise.all([
21-
new SoundCloud().track.get(trackId),
22-
new SoundCloud().track.getStreamUrl(trackId),
23-
]);
17+
audio.onended = async () => {
18+
nextTrack();
19+
};
20+
21+
export async function loadTrack(trackId: number) {
22+
const streamUrl = await player.loadTrack(trackId);
23+
24+
audio.src = streamUrl;
25+
audio.currentTime = 0;
26+
27+
audio.play();
28+
player.update({ playing: true });
29+
}
30+
31+
export async function loadPlaylist(playlistId: number) {
32+
const streamUrl = await player.loadPlaylist(playlistId);
33+
34+
audio.src = streamUrl;
35+
audio.currentTime = 0;
36+
37+
audio.play();
38+
player.update({ playing: true });
39+
}
40+
41+
export async function nextTrack() {
42+
const streamUrl = await player.nextTrack();
43+
if (!streamUrl) return;
2444
2545
audio.src = streamUrl;
2646
audio.currentTime = 0;
2747
28-
player.set({
29-
track,
30-
currentTime: 0,
31-
duration: track.duration || 0,
32-
playing: true,
33-
});
48+
audio.play();
49+
player.update({ playing: true });
50+
}
51+
52+
export async function previousTrack() {
53+
const streamUrl = await player.previousTrack();
54+
if (!streamUrl) return;
55+
56+
audio.src = streamUrl;
57+
audio.currentTime = 0;
3458
3559
audio.play();
60+
player.update({ playing: true });
3661
}
3762
3863
export function play() {
3964
audio.play();
40-
updateStatus({ playing: true });
65+
player.update({ playing: true });
4166
}
4267
4368
export function pause() {
4469
audio.pause();
45-
updateStatus({ playing: false });
70+
player.update({ playing: false });
4671
}
4772
4873
export function stop() {
4974
audio.src = '';
5075
audio.currentTime = 0;
51-
player.set({
52-
track: undefined,
53-
playing: false,
54-
currentTime: 0,
55-
duration: 0,
56-
});
76+
player.reset();
5777
}
5878
5979
export function skip(seconds: number) {
6080
const newTime = clamp(audio.currentTime + seconds, 0, audio.duration);
6181
audio.currentTime = audio.currentTime + seconds;
62-
updateStatus({ currentTime: newTime });
82+
player.update({ currentTime: newTime });
6383
}
6484
6585
export function skipTo(seconds: number) {
6686
const newTime = clamp(seconds, 0, audio.duration);
6787
audio.currentTime = seconds;
68-
updateStatus({ currentTime: newTime });
69-
}
70-
71-
function updateStatus(changes: Partial<PlaybackProgress>) {
72-
player.update((a) => ({
73-
...a,
74-
...changes,
75-
}));
88+
player.update({ currentTime: newTime });
7689
}
7790
</script>

src/components/PlaylistItem.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import ListItem from 'onyx-ui/components/list/ListItem.svelte';
33
import { Onyx } from 'onyx-ui/services';
44
import { push } from 'svelte-spa-router';
5-
import { load } from '../components/AudioPlayer.svelte';
5+
import { loadPlaylist } from '../components/AudioPlayer.svelte';
66
import { SoundCloud } from '../lib/soundcloud';
77
import type { Playlist } from '../models';
88
import { getImage } from '../utils/getImage';
@@ -28,7 +28,7 @@
2828
{
2929
label: 'Play',
3030
onSelect: async () => {
31-
load(playlist.id);
31+
loadPlaylist(playlist.id);
3232
Onyx.contextMenu.close();
3333
},
3434
},

src/components/TrackItem.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import ListItem from 'onyx-ui/components/list/ListItem.svelte';
33
import { Onyx } from 'onyx-ui/services';
44
import { push } from 'svelte-spa-router';
5-
import { load } from '../components/AudioPlayer.svelte';
5+
import { loadTrack } from '../components/AudioPlayer.svelte';
66
import { SoundCloud } from '../lib/soundcloud';
77
import type { Track } from '../models';
88
import { getImage } from '../utils/getImage';
@@ -28,7 +28,7 @@
2828
{
2929
label: 'Play',
3030
onSelect: async () => {
31-
load(track.id);
31+
loadTrack(track.id);
3232
Onyx.contextMenu.close();
3333
},
3434
},

src/models/PlaybackProgress.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Track } from './Track';
22

33
export type PlaybackProgress = {
44
track: Track | undefined;
5+
allTracks: Track[];
56
playing: boolean;
67
currentTime: number;
78
duration: number;

src/routes/Player.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import MdPlayArrow from 'svelte-icons/md/MdPlayArrow.svelte';
1818
import MdRepeat from 'svelte-icons/md/MdRepeat.svelte';
1919
import { replace } from 'svelte-spa-router';
20-
import { pause, play, skip, skipTo } from '../components/AudioPlayer.svelte';
20+
import { pause, play, previousTrack, skip, skipTo } from '../components/AudioPlayer.svelte';
2121
import Waveform from '../components/Waveform.svelte';
2222
import { player } from '../stores/player';
2323
import { formatTime } from '../utils/formatTime';
@@ -50,7 +50,13 @@
5050
return true;
5151
},
5252
onArrowLeftLong: () => {
53-
skipTo(0);
53+
console.log('time', $player.currentTime);
54+
55+
if ($player.currentTime < 10) {
56+
previousTrack();
57+
} else {
58+
skipTo(0);
59+
}
5460
return true;
5561
},
5662
onArrowLeft: () => {

src/routes/Playlist.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import MdInfoOutline from 'svelte-icons/md/MdInfoOutline.svelte';
1414
import MdPlayarrow from 'svelte-icons/md/MdPlayarrow.svelte';
1515
import { push } from 'svelte-spa-router';
16+
import { loadPlaylist } from '../components/AudioPlayer.svelte';
1617
import TrackItem from '../components/TrackItem.svelte';
1718
import { SoundCloud } from '../lib/soundcloud';
1819
import { getImage } from '../utils/getImage';
@@ -49,7 +50,7 @@
4950
color={Color.Primary}
5051
navi={{
5152
itemId: `btnPlayAll`,
52-
onSelect: async () => console.log('play all'),
53+
onSelect: async () => loadPlaylist(data.id),
5354
}}
5455
/>
5556
<IconButton

src/routes/Track.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import { registerView, updateView } from 'onyx-ui/stores/view';
1212
import { onMount } from 'svelte';
1313
import MdPlayarrow from 'svelte-icons/md/MdPlayarrow.svelte';
14-
import { load } from '../components/AudioPlayer.svelte';
14+
import { loadTrack } from '../components/AudioPlayer.svelte';
1515
import { SoundCloud } from '../lib/soundcloud';
1616
import { formatTime } from '../utils/formatTime';
1717
import { getImage } from '../utils/getImage';
@@ -48,7 +48,7 @@
4848
color={Color.Primary}
4949
navi={{
5050
itemId: `btnPlay`,
51-
onSelect: async () => load(data.id),
51+
onSelect: async () => loadTrack(data.id),
5252
}}
5353
/>
5454
<Divider title="stats" />

src/stores/player.ts

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,103 @@
1-
import { writable } from 'svelte/store';
1+
import { get, writable } from 'svelte/store';
2+
import { SoundCloud } from '../lib/soundcloud';
23
import type { PlaybackProgress } from '../models';
34

4-
export const player = writable<PlaybackProgress>({
5-
track: undefined,
6-
playing: false,
7-
currentTime: 0,
8-
duration: 0,
9-
});
5+
function createPlayer() {
6+
const { subscribe, update, set } = writable<PlaybackProgress>({
7+
track: undefined,
8+
allTracks: [],
9+
playing: false,
10+
currentTime: 0,
11+
duration: 0,
12+
});
13+
14+
return {
15+
subscribe,
16+
update: function (data: Partial<PlaybackProgress>) {
17+
update((previous) => ({ ...previous, ...data }));
18+
},
19+
reset: function () {
20+
set({
21+
track: undefined,
22+
allTracks: [],
23+
playing: false,
24+
currentTime: 0,
25+
duration: 0,
26+
});
27+
},
28+
loadTrack: async function (trackId: number): Promise<string> {
29+
const [track, streamUrl] = await Promise.all([
30+
new SoundCloud().track.get(trackId),
31+
new SoundCloud().track.getStreamUrl(trackId),
32+
]);
33+
34+
set({
35+
track,
36+
allTracks: [track],
37+
playing: false,
38+
currentTime: 0,
39+
duration: 0,
40+
});
41+
42+
return streamUrl;
43+
},
44+
loadPlaylist: async function (playlistId: number): Promise<string> {
45+
const playlist = await new SoundCloud().playlist.get(playlistId, true);
46+
47+
if (playlist.tracks.length === 0) return;
48+
49+
const [track, streamUrl] = await Promise.all([
50+
new SoundCloud().track.get(playlist.tracks[0].id),
51+
new SoundCloud().track.getStreamUrl(playlist.tracks[0].id),
52+
]);
53+
54+
set({
55+
track,
56+
allTracks: playlist.tracks,
57+
playing: false,
58+
currentTime: 0,
59+
duration: 0,
60+
});
61+
62+
return streamUrl;
63+
},
64+
nextTrack: async function (): Promise<string | null> {
65+
const current = get({ subscribe });
66+
const index = current.allTracks.findIndex((a) => a.id === current.track.id);
67+
const track = current.allTracks[index + 1];
68+
if (!track || index === -1) return null;
69+
70+
const streamUrl = await new SoundCloud().track.getStreamUrl(track.id);
71+
72+
update((previous) => ({
73+
...previous,
74+
track: track,
75+
playing: false,
76+
currentTime: 0,
77+
duration: 0,
78+
}));
79+
80+
return streamUrl;
81+
},
82+
previousTrack: async function (): Promise<string | null> {
83+
const current = get({ subscribe });
84+
const index = current.allTracks.findIndex((a) => a.id === current.track.id);
85+
const track = current.allTracks[index - 1];
86+
if (!track || index === -1) return null;
87+
88+
const streamUrl = await new SoundCloud().track.getStreamUrl(track.id);
89+
90+
update((previous) => ({
91+
...previous,
92+
track: track,
93+
playing: false,
94+
currentTime: 0,
95+
duration: 0,
96+
}));
97+
98+
return streamUrl;
99+
},
100+
};
101+
}
102+
103+
export const player = createPlayer();

0 commit comments

Comments
 (0)