- 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": {
"bootstrap-icons": "^1.11.2",
"dompurify": "^3.0.6",
"peerjs": "^1.5.1",
"peerjs": "^1.5.2",
"pinia": "^2.1.7",
"shaka-player": "^4.6.1",
"sortablejs": "^1.15.0",
"shaka-player": "^4.7.1",
"sortablejs": "^1.15.1",
"vue": "^3.2.38"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"prettier": "^3.1.0",
"vite": "^5.0.2",
"vite-plugin-pwa": "^0.17.0"
"@vitejs/plugin-vue": "^4.5.2",
"prettier": "^3.1.1",
"vite": "^5.0.7",
"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')),
Library = defineAsyncComponent(() => import('@/components/Library.vue')),
Prefs = defineAsyncComponent(() => import('@/components/Prefs.vue')),
RestorePrefs = defineAsyncComponent(() =>
import('@/components/RestorePrefs.vue'),
RestorePrefs = defineAsyncComponent(
() => import('@/components/RestorePrefs.vue'),
);
/* 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,
song: String,
title: String,
artist: String,
}),
emit = defineEmits(['show']);
@ -29,6 +30,7 @@ const pl = ref(''),
const url = () => props.song || data.state.url,
title = () => props.title || data.state.title,
artist = () => props.artist || data.state.artist,
show = {
get is() {
return props.song ? true : player.state.add;
@ -43,9 +45,13 @@ function Save() {
if (plRemote.value == true && store.auth) {
useAuthAddToPlaylist(pl.value, url());
} else if (plRemote.value == false) {
useUpdatePlaylist(pl.value, { url: url(), title: title() }, e => {
if (e === true) console.log('Added Song');
});
useUpdatePlaylist(
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 { useAlert } from '@/stores/misc';
let shaka;
const player = usePlayer(),
data = useData(),
store = useStore(),
@ -25,11 +27,11 @@ function audioCanPlay() {
}
async function Stream() {
const res = player.state,
shaka = await import('shaka-player/dist/shaka-player.compiled.js').then(
mod => mod.default,
);
const res = player.state;
shaka ??= await import('shaka-player/dist/shaka-player.compiled.js').then(
mod => mod.default,
);
const { url, mime } = await useManifest(res);
@ -40,7 +42,7 @@ async function Stream() {
const audioPlayer = new shaka.Player(),
codecs = store.getItem('codec');
await audioPlayer.attach(audio.value)
await audioPlayer.attach(audio.value);
audioPlayer.getNetworkingEngine().registerRequestFilter((_type, req) => {
const headers = req.headers;
@ -110,7 +112,7 @@ async function Stream() {
})
.catch(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);
});
}

View file

@ -2,14 +2,17 @@
import { ref, onMounted } from 'vue';
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 { useI18n } from '@/stores/misc.js';
import { useI18n, useAlert } from '@/stores/misc.js';
const emit = defineEmits(['playthis']);
const { t } = useI18n(),
player = usePlayer(),
data = useData();
data = useData(),
{ add: addal } = useAlert();
const pl = ref(null);
@ -39,6 +42,35 @@ onMounted(() => {
ref="pl"
class="pl-modal placeholder"
: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
v-for="plurl in data.state.urls"
class="pl-item"
@ -87,21 +119,40 @@ onMounted(() => {
.placeholder:empty::before {
--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 {
display: flex;
align-items: center;
min-height: 3.55rem;
column-gap: 0.4rem;
padding: 0.4rem;
margin: 0.125rem;
border-radius: 0.25rem;
background: var(--color-background);
margin-left: 0;
margin-right: 0;
transition: background-color 0.1s ease;
}
.pl-item:hover {
.pl-item:hover,
.pl-btn:hover {
background: var(--color-background-soft);
}
.pl-item:active {
.pl-item:active,
.pl-btn:active {
background: var(--color-border);
}
.pl-main {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -93,7 +93,9 @@ export function useMetadata(url, urls, data) {
export async function useManifest({ streams, duration, hls }) {
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 dash = useDash(streams, duration);
@ -110,3 +112,19 @@ export async function useManifest({ streams, duration, hls }) {
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;
}