mirror of
https://codeberg.org/Hyperpipe/Hyperpipe
synced 2025-06-27 20:58:01 +02:00
changes:
- 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:
parent
d1592eef0c
commit
129f14c64f
13 changed files with 541 additions and 452 deletions
773
package-lock.json
generated
773
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"date": "2023-11-18"
|
||||
"date": "2023-12-12"
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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="
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue