- added shuffle/save/clear to queue
- custom piped api (1/2 #154)
- track numbers for songs in album/playlist (see #147)
- use dash if managed mse is available
- store artist name for local playlist
This commit is contained in:
Shiny Nematoda 2023-12-12 09:31:31 +00:00
parent d1592eef0c
commit 129f14c64f
13 changed files with 541 additions and 452 deletions

773
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,16 +12,16 @@
"dependencies": { "dependencies": {
"bootstrap-icons": "^1.11.2", "bootstrap-icons": "^1.11.2",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"peerjs": "^1.5.1", "peerjs": "^1.5.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"shaka-player": "^4.6.1", "shaka-player": "^4.7.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.1",
"vue": "^3.2.38" "vue": "^3.2.38"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^4.5.2",
"prettier": "^3.1.0", "prettier": "^3.1.1",
"vite": "^5.0.2", "vite": "^5.0.7",
"vite-plugin-pwa": "^0.17.0" "vite-plugin-pwa": "^0.17.4"
} }
} }

View file

@ -25,8 +25,8 @@ const Genres = defineAsyncComponent(() => import('@/components/Genres.vue')),
Charts = defineAsyncComponent(() => import('@/components/Charts.vue')), Charts = defineAsyncComponent(() => import('@/components/Charts.vue')),
Library = defineAsyncComponent(() => import('@/components/Library.vue')), Library = defineAsyncComponent(() => import('@/components/Library.vue')),
Prefs = defineAsyncComponent(() => import('@/components/Prefs.vue')), Prefs = defineAsyncComponent(() => import('@/components/Prefs.vue')),
RestorePrefs = defineAsyncComponent(() => RestorePrefs = defineAsyncComponent(
import('@/components/RestorePrefs.vue'), () => import('@/components/RestorePrefs.vue'),
); );
/* Composables */ /* Composables */

View file

@ -1,3 +1,3 @@
{ {
"date": "2023-11-18" "date": "2023-12-12"
} }

View file

@ -19,6 +19,7 @@ const props = defineProps({
show: Boolean, show: Boolean,
song: String, song: String,
title: String, title: String,
artist: String,
}), }),
emit = defineEmits(['show']); emit = defineEmits(['show']);
@ -29,6 +30,7 @@ const pl = ref(''),
const url = () => props.song || data.state.url, const url = () => props.song || data.state.url,
title = () => props.title || data.state.title, title = () => props.title || data.state.title,
artist = () => props.artist || data.state.artist,
show = { show = {
get is() { get is() {
return props.song ? true : player.state.add; return props.song ? true : player.state.add;
@ -43,9 +45,13 @@ function Save() {
if (plRemote.value == true && store.auth) { if (plRemote.value == true && store.auth) {
useAuthAddToPlaylist(pl.value, url()); useAuthAddToPlaylist(pl.value, url());
} else if (plRemote.value == false) { } else if (plRemote.value == false) {
useUpdatePlaylist(pl.value, { url: url(), title: title() }, e => { useUpdatePlaylist(
if (e === true) console.log('Added Song'); pl.value,
}); { url: url(), title: title(), artist: artist() },
e => {
if (e === true) console.log('Added Song');
},
);
} }
} }
} }

View file

@ -5,6 +5,8 @@ import { useStore, useRoute, useManifest } from '@/scripts/util.js';
import { useData, usePlayer } from '@/stores/player.js'; import { useData, usePlayer } from '@/stores/player.js';
import { useAlert } from '@/stores/misc'; import { useAlert } from '@/stores/misc';
let shaka;
const player = usePlayer(), const player = usePlayer(),
data = useData(), data = useData(),
store = useStore(), store = useStore(),
@ -25,11 +27,11 @@ function audioCanPlay() {
} }
async function Stream() { async function Stream() {
const res = player.state;
const res = player.state,
shaka = await import('shaka-player/dist/shaka-player.compiled.js').then( shaka ??= await import('shaka-player/dist/shaka-player.compiled.js').then(
mod => mod.default, mod => mod.default,
); );
const { url, mime } = await useManifest(res); const { url, mime } = await useManifest(res);
@ -40,7 +42,7 @@ async function Stream() {
const audioPlayer = new shaka.Player(), const audioPlayer = new shaka.Player(),
codecs = store.getItem('codec'); codecs = store.getItem('codec');
await audioPlayer.attach(audio.value) await audioPlayer.attach(audio.value);
audioPlayer.getNetworkingEngine().registerRequestFilter((_type, req) => { audioPlayer.getNetworkingEngine().registerRequestFilter((_type, req) => {
const headers = req.headers; const headers = req.headers;
@ -110,7 +112,7 @@ async function Stream() {
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
if (err.code == 3016) a.add('MediaError: ' + err.data[0]) if (err.code == 3016) a.add('MediaError: ' + err.data[0]);
else a.add('Error: ' + err.code); else a.add('Error: ' + err.code);
}); });
} }

View file

@ -2,14 +2,17 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import Sortable from 'sortablejs/modular/sortable.core.esm.js'; import Sortable from 'sortablejs/modular/sortable.core.esm.js';
import { useShuffle } from '@/scripts/util.js';
import { useCreatePlaylist } from '@/scripts/db.js';
import { useData, usePlayer } from '@/stores/player.js'; import { useData, usePlayer } from '@/stores/player.js';
import { useI18n } from '@/stores/misc.js'; import { useI18n, useAlert } from '@/stores/misc.js';
const emit = defineEmits(['playthis']); const emit = defineEmits(['playthis']);
const { t } = useI18n(), const { t } = useI18n(),
player = usePlayer(), player = usePlayer(),
data = useData(); data = useData(),
{ add: addal } = useAlert();
const pl = ref(null); const pl = ref(null);
@ -39,6 +42,35 @@ onMounted(() => {
ref="pl" ref="pl"
class="pl-modal placeholder" class="pl-modal placeholder"
:data-placeholder="t('playlist.add')"> :data-placeholder="t('playlist.add')">
<div v-if="data.state.urls && data.state.urls.length > 0" class="pl-bar">
<button
class="bi bi-shuffle pl-btn"
title="shuffle queue"
@click="
() => {
data.state.urls = useShuffle(data.state.urls);
$emit('playthis', data.state.urls[0]);
}
"></button>
<button
class="bi bi-bookmark-plus pl-btn"
title="save queue"
@click="
() => {
let urls = data.state.urls.map(i => ({
title: i.title,
url: i.url,
}));
useCreatePlaylist(new Date().toISOString(), urls, () =>
addal(t('info.saved')),
);
}
"></button>
<button
class="bi bi-dash-lg pl-btn"
title="clear queue"
@click="data.state.urls = []"></button>
</div>
<div <div
v-for="plurl in data.state.urls" v-for="plurl in data.state.urls"
class="pl-item" class="pl-item"
@ -87,21 +119,40 @@ onMounted(() => {
.placeholder:empty::before { .placeholder:empty::before {
--ico: '\f64d'; --ico: '\f64d';
} }
.pl-bar {
display: flex;
margin-bottom: 0.25rem;
}
.pl-btn {
height: 2.5rem;
aspect-ratio: 1;
line-height: 2.5rem;
}
.pl-item,
.pl-btn {
border-radius: 0.35rem;
margin: 0.125rem;
background: var(--color-background);
}
.pl-btn:first-child + .pl-btn {
margin-left: auto;
}
.pl-item { .pl-item {
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 3.55rem; min-height: 3.55rem;
column-gap: 0.4rem; column-gap: 0.4rem;
padding: 0.4rem; padding: 0.4rem;
margin: 0.125rem; margin-left: 0;
border-radius: 0.25rem; margin-right: 0;
background: var(--color-background);
transition: background-color 0.1s ease; transition: background-color 0.1s ease;
} }
.pl-item:hover { .pl-item:hover,
.pl-btn:hover {
background: var(--color-background-soft); background: var(--color-background-soft);
} }
.pl-item:active { .pl-item:active,
.pl-btn:active {
background: var(--color-border); background: var(--color-border);
} }
.pl-main { .pl-main {

View file

@ -25,20 +25,13 @@ const { t, setupLocale } = useI18n(),
compact = ref(false), compact = ref(false),
prm = ref(false), prm = ref(false),
cc = ref(false), cc = ref(false),
restoreUrl = ref(''); restoreUrl = ref(''),
prompt = txt => window.prompt(txt);
getJson('https://piped-instances.kavin.rocks') getJson('https://piped-instances.kavin.rocks').then(i => (instances.value = i));
.then(i => i || getJson('https://instances.tokhmi.xyz'))
.then(i => {
instances.value = i;
console.log(i);
});
getJson('https://raw.codeberg.page/Hyperpipe/pages/api/backend.json').then( getJson('https://raw.codeberg.page/Hyperpipe/pages/api/backend.json').then(
i => { i => (hypInstances.value = i),
hypInstances.value = i;
console.log(i);
},
); );
const getRestoreUrl = () => { const getRestoreUrl = () => {
@ -317,7 +310,12 @@ onMounted(() => {
v-if="instances" v-if="instances"
class="input" class="input"
:value="getStore('pipedapi') || PIPED_INSTANCE" :value="getStore('pipedapi') || PIPED_INSTANCE"
@change="setStore('pipedapi', $event.target.value)"> @change="
e => {
const v = e.target.value;
setStore('pipedapi', v == 'x' ? prompt('instance') : v);
}
">
<option <option
v-for="i in instances" v-for="i in instances"
:key="i.name" :key="i.name"
@ -328,6 +326,8 @@ onMounted(() => {
<option v-if="!verifyPipedApi"> <option v-if="!verifyPipedApi">
{{ getStore('pipedapi') || PIPED_INSTANCE }} {{ getStore('pipedapi') || PIPED_INSTANCE }}
</option> </option>
<option value="x">Custom</option>
</select> </select>
<h3>{{ t('instances.auth') }}</h3> <h3>{{ t('instances.auth') }}</h3>

View file

@ -15,7 +15,7 @@ const params = ref({}),
onMounted(() => { onMounted(() => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
urlParams.forEach((val, key) => { urlParams.forEach((val, key) => {
params.value[key] = val params.value[key] = val;
}); });
}); });
</script> </script>

View file

@ -17,7 +17,13 @@ import {
getPipedQuery, getPipedQuery,
useAuthRemovePlaylist, useAuthRemovePlaylist,
} from '@/scripts/fetch.js'; } from '@/scripts/fetch.js';
import { useVerifyAuth, useRoute, useWrap, useShare } from '@/scripts/util.js'; import {
useVerifyAuth,
useRoute,
useWrap,
useShare,
useShuffle,
} from '@/scripts/util.js';
import { useCreatePlaylist, useRemovePlaylist } from '@/scripts/db.js'; import { useCreatePlaylist, useRemovePlaylist } from '@/scripts/db.js';
import { useResults, useArtist } from '@/stores/results.js'; import { useResults, useArtist } from '@/stores/results.js';
@ -42,27 +48,15 @@ const plId = computed(
const shuffleAdd = () => { const shuffleAdd = () => {
const songs = results.items.songs.items.map(i => ({ const songs = results.items.songs.items.map(i => ({
url: i.url, url: i.url,
title: i.title, title: i.title,
thumbnails: [{ url: i.thumbnail }], thumbnails: [{ url: i.thumbnail }],
thumbnail: i.thumbnail, thumbnail: i.thumbnail,
offlineUri: i.offlineUri, offlineUri: i.offlineUri,
duration: i.duration, duration: i.duration,
})), }));
copy = [];
let nos = songs.length; emit('play-urls', useShuffle(songs));
while (nos) {
const i = Math.floor(Math.random() * nos--);
copy.push(songs[i]);
songs[i] = songs[nos];
delete songs[nos];
}
emit('play-urls', copy);
}, },
openSong = (song, nxt = false) => { openSong = (song, nxt = false) => {
if (results.items?.songs?.title && !nxt) { if (results.items?.songs?.title && !nxt) {
@ -112,6 +106,7 @@ const shuffleAdd = () => {
const urls = results.items?.songs?.items?.map(item => ({ const urls = results.items?.songs?.items?.map(item => ({
url: item.url, url: item.url,
title: item.title, title: item.title,
artist: item.uploaderName || item.artist || item.subtitle,
})); }));
let title = results.items?.songs?.title; let title = results.items?.songs?.title;
@ -339,7 +334,9 @@ onDeactivated(() => {
:author="song.uploaderName || song.artist || song.subtitle" :author="song.uploaderName || song.artist || song.subtitle"
:title="song.title || song.name" :title="song.title || song.name"
:channel=" :channel="
song.uploaderUrl || song.artistUrl || '/channel/' + song.subId song.uploaderUrl ||
song.artistUrl ||
(song.subId && '/channel/' + song.subId)
" "
:play="song.url || '/watch?v=' + song.id" :play="song.url || '/watch?v=' + song.id"
:art=" :art="

View file

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref } from 'vue';
import AddToPlaylist from '@/components/AddToPlaylist.vue'; import AddToPlaylist from '@/components/AddToPlaylist.vue';
@ -97,16 +97,13 @@ const openSong = el => {
useShare(data); useShare(data);
}; };
onMounted(() => {
console.log(props.channel, artist.state.playlistId);
});
</script> </script>
<template> <template>
<AddToPlaylist <AddToPlaylist
v-if="showPl" v-if="showPl"
:song="play" :song="play"
:title="title" :title="title"
:artist="author"
@show="e => (showPl = e)" /> @show="e => (showPl = e)" />
<div <div
@ -117,7 +114,12 @@ onMounted(() => {
<img class="pop-2 bg-img song-bg" loading="lazy" :src="art" alt /> <img class="pop-2 bg-img song-bg" loading="lazy" :src="art" alt />
<span class="flex content"> <span class="flex content">
<h4>{{ title }}</h4> <h4>
<template v-if="results.items?.songs?.title"
>{{ index + 1 }}.
</template>
{{ title }}
</h4>
<a <a
class="ign" class="ign"
:href="channel" :href="channel"

View file

@ -69,7 +69,7 @@ export function useCreatePlaylist(key, obj, cb = () => null) {
cb(res); cb(res);
} else { } else {
console.error(e.target.result); console.error(res);
alert(`Error: Playlist with name ${key} exists`); alert(`Error: Playlist with name ${key} exists`);
} }
}; };
@ -125,9 +125,7 @@ export function useListPlaylists(cb = () => null) {
if (pl) { if (pl) {
pls.push(pl.value); pls.push(pl.value);
pl.continue(); pl.continue();
} else { } else cb(pls);
cb(pls);
}
}; };
} }
} }

View file

@ -93,7 +93,9 @@ export function useMetadata(url, urls, data) {
export async function useManifest({ streams, duration, hls }) { export async function useManifest({ streams, duration, hls }) {
let url, mime; let url, mime;
if (window.MediaSource !== undefined && streams.length > 0) { const mse = window.MediaSource || window.ManagedMediaSource;
if (mse !== undefined && streams.length > 0) {
const { useDash } = await import('./dash.js'); const { useDash } = await import('./dash.js');
const dash = useDash(streams, duration); const dash = useDash(streams, duration);
@ -110,3 +112,19 @@ export async function useManifest({ streams, duration, hls }) {
return { url, mime }; return { url, mime };
} }
export function useShuffle(songs) {
let copy = [],
nos = songs.length;
while (nos) {
const i = Math.floor(Math.random() * nos--);
copy.push(songs[i]);
songs[i] = songs[nos];
delete songs[nos];
}
return copy;
}