Added Sync for Playlists

This commit is contained in:
Shiny Nematoda 2022-06-05 20:35:58 +05:30
parent a88c9081f1
commit e867ab25e9
11 changed files with 287 additions and 41 deletions

View file

@ -17,6 +17,8 @@ A Privacy Respecting Frontend for YouTube Music inspired and built with the help
_But if you see any of the following, Please open an issue_
PS: Please don't forget to support your favorite artists :)
## Usage
```sh
@ -70,16 +72,26 @@ You can reach out to me personally on:
- VueJS -> [MIT][vue]
- ViteJS -> [MIT][vite]
- hls.js -> [APACHE][hls]
- PeerJS -> [MIT][peer]
- Bootstrap Icons -> [MIT][bi]
- VueJS theme -> [MIT][vuetheme]
- Nord theme -> [MIT][nord]
### Similar Projects
*Hyperpipe is not affiliated with any of these projects*
- [Beatbump](https://github.com/snuffyDev/Beatbump) -> Alternative YouTube Music frontend built with Svelte/SvelteKit
- [Cider](https://github.com/ciderapp/Cider) -> Cross-platform Apple Music experience based on Electron and Vue.js
[hypipe]: https://hyperpipe.surge.sh
[piped]: https://piped.kavin.rocks
[license]: https://codeberg.org/Hyperpipe/Hyperpipe/src/branch/main/LICENSE.txt
[vue]: https://github.com/vuejs/core/blob/main/LICENSE
[vite]: https://github.com/vitejs/vite/blob/main/LICENSE
[bi]: https://github.com/twbs/icons/blob/main/LICENSE.md
[peer]: https://github.com/peers/peerjs/blob/master/LICENSE
[hls]: https://github.com/video-dev/hls.js/blob/master/LICENSE
[nord]: https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md
[vuetheme]: https://github.com/vuejs/theme/blob/main/LICENSE

View file

@ -23,6 +23,9 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@latest/font/bootstrap-icons.css" />
<script type="module" src="/src/main.js"></script>
<script
src="https://unpkg.com/peerjs@1.3.2/dist/peerjs.min.js"
defer></script>
<script type="module" src="/src/main.js" defer></script>
</body>
</html>

View file

@ -220,6 +220,17 @@ async function getNext(hash) {
!data.urls.filter(s => s.url == data.url)[0] ||
data.urls.length == 1
) {
if (useStore().getItem('next') == 'false') {
data.urls = [
{
title: data.nowtitle,
url: '/watch?v=' + hash,
},
];
setMetadata();
return;
}
const json = await getJson(
'https://hyperpipeapi.onrender.com/next/' + hash,
);
@ -347,9 +358,8 @@ onBeforeMount(() => {
});
onMounted(() => {
if (window.hls) {
window.hls.destroy()
window.hls.destroy();
}
useLazyLoad();

View file

@ -234,7 +234,7 @@ button {
}
.bars-wrap {
left: .75rem;
left: 0.75rem;
position: absolute;
height: 1.5rem;
width: 2rem;

View file

@ -62,7 +62,7 @@ watch(show, n => {
}
.modal-content * {
width: 100%;
padding: .5rem 1rem;
padding: 0.5rem 1rem;
}
.modal-close {
color: var(--color-background);

View file

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue';
import { ref, reactive, watch, onMounted } from 'vue';
import AlbumItem from './AlbumItem.vue';
import Modal from './Modal.vue';
@ -9,13 +9,24 @@ import {
useListPlaylists,
useGetPlaylist,
useCreatePlaylist,
useUpdatePlaylist,
} from '../scripts/db.js';
const emit = defineEmits(['play-urls']),
list = ref([]),
show = ref(false),
show = reactive({
new: false,
sync: false,
}),
text = ref(''),
Play = key => {
sync = reactive({
type: 'send',
id: 'Please Wait...',
to: '',
peer: undefined,
});
const Play = key => {
console.log(key);
useGetPlaylist(key, res => {
@ -32,25 +43,90 @@ const emit = defineEmits(['play-urls']),
if (text.value) {
useCreatePlaylist(text.value, [], () => {
List();
show.value = false;
show.new = false;
});
}
},
Send = () => {
const conn = sync.peer.connect(sync.to);
console.log(conn);
conn.on('open', () => {
List();
conn.send(list.value);
});
conn.on('close', () => {
show.sync = false;
});
conn.on('error', err => {
console.log(err);
});
};
onMounted(() => {
List();
});
watch(
() => show.sync,
() => {
if (show.sync === true) {
sync.peer = new Peer('hyp-' + Math.random().toString(36).substr(2));
sync.peer.on('open', id => {
sync.id = id;
});
sync.peer.on('connection', conn => {
console.log(conn);
conn.on('data', data => {
if (sync.type == 'rec') {
console.log(data);
List();
for (let i of data) {
const pl = list.value.filter(p => p.name == i.name)[0];
if (pl) {
for (let u of i.urls) {
if (!pl.urls.filter(r => r.url === u.url)[0]) {
useUpdatePlaylist(i.name, u, () => {
console.log('Added: ' + u.name);
});
}
}
} else {
useCreatePlaylist(i.name, i.urls);
}
List();
if (data.indexOf(i) == data.length - 1) {
show.sync = false;
}
}
}
});
});
} else if (sync.peer) {
sync.peer.destroy();
}
},
);
onMounted(List);
</script>
<template>
<div class="npl-wrap">
<Modal
n="2"
:display="show"
:display="show.new"
title="Create a new Playlist..."
@show="
e => {
show = e;
show.new = e;
}
">
<template #content>
@ -61,12 +137,62 @@ onMounted(() => {
v-model="text" />
</template>
<template #buttons>
<button @click="show = false">Cancel</button>
<button @click="show.new = false">Cancel</button>
<button @click="Create">Create</button>
</template>
</Modal>
<div class="npl-box bi bi-plus-lg pop" @click="show = true"></div>
<Modal
:n="sync.type == 'send' ? 2 : 1"
:display="show.sync"
title="Sync Playlists..."
@show="
e => {
show.sync = e;
}
">
<template #content>
<div class="tabs">
<button
:data-active="sync.type == 'send'"
@click="sync.type = 'send'">
Send
</button>
<button :data-active="sync.type == 'rec'" @click="sync.type = 'rec'">
Receive
</button>
</div>
<div v-if="sync.type == 'send'">
<input
type="text"
class="textbox"
placeholder="ID ( hyp-xxxxxxxxx )"
@input="sync.to = $event.target.value" />
</div>
<div v-else-if="sync.type == 'rec'">
<pre>ID:</pre>
<pre>{{ sync.id }}</pre>
</div>
</template>
<template #buttons>
<button @click="show.sync = false">Cancel</button>
<button v-if="sync.type == 'send'" @click="Send">
{{ sync.type == 'send' ? 'Send' : 'Recieve' }}
</button>
</template>
</Modal>
<div class="grid">
<div class="npl-box bi bi-plus-lg pop" @click="show.new = true"></div>
<div
class="npl-box bi bi-arrow-repeat pop"
@click="show.sync = true"></div>
</div>
<div class="grid-3">
<template v-for="i in list">
<AlbumItem
@ -84,7 +210,7 @@ onMounted(() => {
padding-bottom: 5rem;
}
.npl-box {
margin: 0 auto 5rem auto;
margin: 0 auto 2rem auto;
border-radius: 0.5rem;
background-color: var(--color-background-mute);
padding: 2rem 3rem;
@ -105,4 +231,32 @@ onMounted(() => {
.text-box {
padding: 2rem;
}
pre {
white-space: pre-wrap;
}
.tabs {
margin: 0.5rem 0 1.5rem 0;
}
.tabs button {
width: calc(100% / 2);
background: var(--color-background);
}
.tabs button[data-active='true'] {
color: var(--color-background);
background: linear-gradient(135deg, cornflowerblue, #88c0d0);
}
.tabs button:first-child {
border-radius: 0.25rem 0 0 0.25rem;
}
.tabs button:last-child {
border-radius: 0 0.25rem 0.25rem 0;
}
@media (min-width: 1024px) {
.npl-box:first-child {
margin: 0 1rem 0 auto;
}
.npl-box:last-child {
margin: 0 auto 0 1rem;
}
}
</style>

View file

@ -1,5 +1,4 @@
<script setup>
defineProps({
url: String,
urls: Array,
@ -7,7 +6,6 @@ defineProps({
});
defineEmits(['playthis']);
</script>
<template>
@ -21,7 +19,10 @@ defineEmits(['playthis']);
<div class="bars"></div>
</span>
<div v-else-if="plurl.thumbnails" class="pl-img">
<img :src="plurl.thumbnails[0].url" :height=" plurl.thumbnails[0].height" :width="plurl.thumbnails[0].width">
<img
:src="plurl.thumbnails[0].url"
:height="plurl.thumbnails[0].height"
:width="plurl.thumbnails[0].width" />
</div>
<span class="pl-main caps">{{ plurl.title }}</span>
</div>
@ -66,22 +67,22 @@ defineEmits(['playthis']);
padding-left: 2.75rem;
}
.pl-img {
top: .45rem;
left: .45rem;
top: 0.45rem;
left: 0.45rem;
position: absolute;
background-image: var(--src);
height: 2.75rem;
width: 2.75rem;
border-radius: .125rem;
border-radius: 0.125rem;
background-size: contain;
background-repeat: no-repeat;
}
.pl-img img {
height: 100%;
width: 100%;
border-radius: .125rem;
border-radius: 0.125rem;
}
.pl-img[data-active=false] {
.pl-img[data-active='false'] {
display: none;
}
</style>

View file

@ -1,11 +1,13 @@
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { getJson } from '../scripts/fetch.js';
import { useStore } from '../scripts/util.js';
const instances = ref([]),
hls = ref(false);
hypInstances = ref([]),
hls = ref(false),
next = ref(false);
getJson('https://piped-instances.kavin.rocks').then(i => {
instances.value = i;
@ -13,6 +15,12 @@ getJson('https://piped-instances.kavin.rocks').then(i => {
console.log(i);
});
/*getJson('https://hyperpipe.codeberg.page/api/backend.json').then(i => {
hypInstances.value = i
console.log(i);
});*/
function getBool(val) {
return 'bi ' + (val ? 'bi-check2' : 'bi-x-lg');
}
@ -35,7 +43,14 @@ function setTheme(theme) {
setStore('theme', theme);
}
hls.value = getStore('hls') || true;
function getStoreBool(key, ele) {
ele.value = getStore(key) || true;
}
onMounted(() => {
getStoreBool('hls', hls);
getStoreBool('next', next);
});
</script>
<template>
@ -60,6 +75,16 @@ hls.value = getStore('hls') || true;
<label for="pref-chk-hls">Live Streaming</label>
</div>
<div class="left">
<input
type="checkbox"
name="pref-chk-next"
id="pref-chk-next"
@change="setStore('next', $event.target.checked)"
v-model="next" />
<label for="pref-chk-next">Automatically Queue Songs</label>
</div>
<div class="left">
<input
type="number"
@ -72,6 +97,41 @@ hls.value = getStore('hls') || true;
<label for="pref-volume">Default Volume</label>
</div>
<h2>Hyperpipe Instance</h2>
<select
v-if="hypInstances"
:value="getStore('api') || 'hyperpipeapi.onrender.com'"
@change="setStore('api', $event.target.value)">
<option
v-for="i in hypInstances"
:key="i.name"
:value="i.api_url.replace('https://', '').replace('http://', '')">
{{ i.name }}
</option>
</select>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Name</th>
<th>Locations</th>
</tr>
</thead>
<tbody v-for="i in hypInstances">
<tr>
<td>
{{ i.name }}
</td>
<td>
{{ i.locations.replaceAll(',', '') }}
</td>
</tr>
</tbody>
</table>
</div>
<h2>Piped Instance</h2>
<select
v-if="instances"
@ -79,9 +139,9 @@ hls.value = getStore('hls') || true;
@change="setStore('pipedapi', $event.target.value)">
<option
v-for="i in instances"
:key="i.name.replace('Official', 'Default')"
:key="i.name"
:value="i.api_url.replace('https://', '').replace('http://', '')">
{{ i.name }}
{{ i.name.replace('Official', 'Default') }}
</option>
</select>

View file

@ -47,7 +47,10 @@ function Save() {
">
<template #content>
<template v-for="i in list">
<div class="flex item" @click="pl = i.name" :data-active="pl == i.name">
<div
class="flex item"
@click="pl = i.name"
:data-active="pl == i.name">
<span>{{ i.name }}</span
><span class="ml-auto">{{ i.urls.length || '' }}</span>
</div>
@ -178,14 +181,14 @@ function Save() {
}
.item {
background: var(--color-background);
border-radius: .5rem;
margin: .5rem 0;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.item:hover {
background: var(--color-background-mute);
}
.item[data-active=true] {
.item[data-active='true'] {
color: var(--color-background);
background: linear-gradient(135deg, cornflowerblue, #88c0d0);
}

View file

@ -12,7 +12,7 @@ export function useRand() {
}
export function useRandColor() {
const r = Math.random().toString(16)
const r = Math.random().toString(16);
return '#' + r.substr(r.length - 6)
}
return '#' + r.substr(r.length - 6);
}

View file

@ -1,4 +1,4 @@
import { useStore } from './util.js'
import { useStore } from './util.js';
export function getPipedQuery() {
const papi = new URLSearchParams(location.search).get('pipedapi');
@ -14,12 +14,11 @@ export async function getJson(url) {
const res = await fetch(url)
.then(res => res.json())
.catch(err => {
console.error(err);
alert(err);
});
if (!res.error) {
return res;
} else {
if (res && res.error) {
alert(
res.message
.replaceAll('Video', 'Audio')
@ -27,6 +26,10 @@ export async function getJson(url) {
.replaceAll('watched', 'heard'),
);
console.error(res.message);
} else if (res) {
return res;
} else {
return;
}
}