mirror of
https://github.com/TeamPiped/Piped.git
synced 2024-11-10 10:18:23 +00:00
Merge remote-tracking branch 'origin/master' into toast
This commit is contained in:
commit
defefed9ed
@ -143,6 +143,7 @@ Contributions in any other form are also welcomed.
|
||||
- [Hyperpipe](https://codeberg.org/Hyperpipe/Hyperpipe) - an alternative privacy respecting frontend for YouTube Music.
|
||||
- [Musicale](https://github.com/Bellisario/musicale) - an alternative to YouTube Music, with style.
|
||||
- [ytify](https://github.com/n-ce/ytify) - a complementary minimal audio streaming frontend for YouTube.
|
||||
- [PsTube](https://github.com/prateekmedia/pstube) - Watch and download videos without ads
|
||||
|
||||
## YourKit
|
||||
|
||||
|
@ -2,6 +2,7 @@ server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
error_log off;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
|
@ -7,6 +7,7 @@
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link title="Piped" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml" />
|
||||
<title>Piped</title>
|
||||
<meta name="theme-color" content="#0f0f0f">
|
||||
<meta property="og:title" content="Piped" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/img/icons/favicon-32x32.png" />
|
||||
|
26
package.json
26
package.json
@ -14,11 +14,11 @@
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"buffer": "6.0.3",
|
||||
"dompurify": "3.0.0",
|
||||
"dompurify": "3.0.1",
|
||||
"hotkeys-js": "3.10.1",
|
||||
"javascript-time-ago": "2.5.9",
|
||||
"mux.js": "6.3.0",
|
||||
"shaka-player": "4.3.4",
|
||||
"shaka-player": "4.3.5",
|
||||
"stream-browserify": "3.0.0",
|
||||
"vue": "3.2.47",
|
||||
"vue-i18n": "9.2.2",
|
||||
@ -26,22 +26,22 @@
|
||||
"xml-js": "1.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "2.2.27",
|
||||
"@iconify/json": "2.2.35",
|
||||
"@intlify/vite-plugin-vue-i18n": "6.0.3",
|
||||
"@unocss/preset-icons": "0.50.0",
|
||||
"@unocss/preset-web-fonts": "0.50.0",
|
||||
"@unocss/transformer-directives": "0.50.0",
|
||||
"@unocss/transformer-variant-group": "0.50.0",
|
||||
"@vitejs/plugin-legacy": "4.0.1",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@unocss/preset-icons": "0.50.6",
|
||||
"@unocss/preset-web-fonts": "0.50.6",
|
||||
"@unocss/transformer-directives": "0.50.6",
|
||||
"@unocss/transformer-variant-group": "0.50.6",
|
||||
"@vitejs/plugin-legacy": "4.0.2",
|
||||
"@vitejs/plugin-vue": "4.1.0",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-prettier": "8.7.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"prettier": "2.8.4",
|
||||
"unocss": "0.50.0",
|
||||
"vite": "4.1.4",
|
||||
"unocss": "0.50.6",
|
||||
"vite": "4.2.0",
|
||||
"vite-plugin-eslint": "1.8.1",
|
||||
"vite-plugin-pwa": "0.14.4"
|
||||
},
|
||||
|
25
src/App.vue
25
src/App.vue
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div class="w-full min-h-screen px-1vw py-5 reset" :class="[theme]">
|
||||
<NavBar />
|
||||
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :max="5">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<div class="flex flex-col w-full min-h-screen px-1vw py-5 reset" :class="[theme]">
|
||||
<div class="flex-1">
|
||||
<NavBar />
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :max="5">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<FooterComponent />
|
||||
</div>
|
||||
@ -34,6 +35,14 @@ export default {
|
||||
if (themePref == "auto") this.theme = darkModePreference.matches ? "dark" : "light";
|
||||
else this.theme = themePref;
|
||||
|
||||
// Change title bar color based on user's theme
|
||||
const themeColor = document.querySelector("meta[name='theme-color']");
|
||||
if (this.theme === "light") {
|
||||
themeColor.setAttribute("content", "#FFF");
|
||||
} else {
|
||||
themeColor.setAttribute("content", "#0F0F0F");
|
||||
}
|
||||
|
||||
// Used for the scrollbar
|
||||
const root = document.querySelector(":root");
|
||||
this.theme == "dark" ? root.classList.add("dark") : root.classList.remove("dark");
|
||||
|
@ -1,43 +1,64 @@
|
||||
<template>
|
||||
<ErrorHandler v-if="channel && channel.error" :message="channel.message" :error="channel.error" />
|
||||
|
||||
<div v-if="channel" v-show="!channel.error">
|
||||
<div class="flex justify-center place-items-center">
|
||||
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
|
||||
<h1 v-text="channel.name" />
|
||||
<font-awesome-icon class="ml-1.5 !text-3xl" v-if="channel.verified" icon="check" />
|
||||
<LoadingIndicatorPage :show-content="channel != null && !channel.error">
|
||||
<img
|
||||
v-if="channel.bannerUrl"
|
||||
:src="channel.bannerUrl"
|
||||
class="w-full py-1.5 h-30 md:h-50 object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<div class="flex place-items-center">
|
||||
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
|
||||
<div class="flex gap-1 items-center">
|
||||
<h1 v-text="channel.name" class="!text-xl" />
|
||||
<font-awesome-icon class="!text-xl" v-if="channel.verified" icon="check" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
@click="subscribeHandler"
|
||||
v-t="{
|
||||
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
|
||||
args: { count: numberFormat(channel.subscriberCount) },
|
||||
}"
|
||||
></button>
|
||||
|
||||
<!-- RSS Feed button -->
|
||||
<a
|
||||
aria-label="RSS feed"
|
||||
title="RSS feed"
|
||||
role="button"
|
||||
v-if="channel.id"
|
||||
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
|
||||
target="_blank"
|
||||
class="btn flex-col"
|
||||
>
|
||||
<font-awesome-icon icon="rss" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img v-if="channel.bannerUrl" :src="channel.bannerUrl" class="w-full pb-1.5" loading="lazy" />
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p class="whitespace-pre-wrap">
|
||||
<span v-html="purifyHTML(rewriteDescription(channel.description))" />
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="btn"
|
||||
@click="subscribeHandler"
|
||||
v-t="{
|
||||
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
|
||||
args: { count: numberFormat(channel.subscriberCount) },
|
||||
}"
|
||||
></button>
|
||||
|
||||
<!-- RSS Feed button -->
|
||||
<a
|
||||
aria-label="RSS feed"
|
||||
title="RSS feed"
|
||||
role="button"
|
||||
v-if="channel.id"
|
||||
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
|
||||
target="_blank"
|
||||
class="btn flex-col mx-3"
|
||||
>
|
||||
<font-awesome-icon icon="rss" />
|
||||
</a>
|
||||
<div v-if="channel.description" class="whitespace-pre-wrap py-2 mx-1">
|
||||
<span v-if="fullDescription" v-html="purifyHTML(rewriteDescription(channel.description))" />
|
||||
<span v-html="purifyHTML(rewriteDescription(channel.description.slice(0, 100)))" v-else />
|
||||
<span v-if="channel.description.length > 100 && !fullDescription">...</span>
|
||||
<button
|
||||
v-if="channel.description.length > 100"
|
||||
class="hover:underline font-semibold text-neutral-500 block whitespace-normal"
|
||||
@click="fullDescription = !fullDescription"
|
||||
>
|
||||
[{{ fullDescription ? $t("actions.show_less") : $t("actions.show_more") }}]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<WatchOnYouTubeButton :link="`https://youtube.com/channel/${this.channel.id}`" />
|
||||
|
||||
<div class="flex mt-4 mb-2">
|
||||
<div class="flex my-2 mx-1">
|
||||
<button
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.name"
|
||||
@ -61,19 +82,21 @@
|
||||
hide-channel
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ErrorHandler from "./ErrorHandler.vue";
|
||||
import ContentItem from "./ContentItem.vue";
|
||||
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorHandler,
|
||||
ContentItem,
|
||||
WatchOnYouTubeButton,
|
||||
LoadingIndicatorPage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -82,6 +105,7 @@ export default {
|
||||
tabs: [],
|
||||
selectedTab: 0,
|
||||
contentItems: [],
|
||||
fullDescription: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@ -121,7 +145,9 @@ export default {
|
||||
});
|
||||
},
|
||||
async fetchChannel() {
|
||||
const url = this.apiUrl() + "/" + this.$route.params.path + "/" + this.$route.params.channelId;
|
||||
const url = this.$route.path.includes("@")
|
||||
? this.apiUrl() + "/c/" + this.$route.params.channelId
|
||||
: this.apiUrl() + "/" + this.$route.params.path + "/" + this.$route.params.channelId;
|
||||
return await this.fetchJson(url);
|
||||
},
|
||||
async getChannelData() {
|
||||
|
@ -24,27 +24,29 @@
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="video-grid">
|
||||
<LoadingIndicatorPage :show-content="videosStore != null" class="video-grid">
|
||||
<template v-for="video in videos" :key="video.url">
|
||||
<VideoItem v-if="shouldShowVideo(video)" :is-feed="true" :item="video" />
|
||||
</template>
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VideoItem from "./VideoItem.vue";
|
||||
import SortingSelector from "./SortingSelector.vue";
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VideoItem,
|
||||
SortingSelector,
|
||||
LoadingIndicatorPage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentVideoCount: 0,
|
||||
videoStep: 100,
|
||||
videosStore: [],
|
||||
videosStore: null,
|
||||
videos: [],
|
||||
availableFilters: ["all", "shorts", "videos"],
|
||||
selectedFilter: "all",
|
||||
|
55
src/components/LoadingIndicatorPage.vue
Normal file
55
src/components/LoadingIndicatorPage.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div v-if="!showContent" class="flex min-h-[75vh] w-full justify-center items-center">
|
||||
<span id="spinner" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#spinner:after {
|
||||
--spinner-color: #000;
|
||||
}
|
||||
|
||||
.dark #spinner:after {
|
||||
--spinner-color: #fff;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
display: inline-block;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
#spinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--spinner-color);
|
||||
border-color: var(--spinner-color) transparent var(--spinner-color) transparent;
|
||||
animation: spinner 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
showContent: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
|
||||
|
||||
<div v-if="playlist" v-show="!playlist.error">
|
||||
<LoadingIndicatorPage :show-content="playlist" v-show="!playlist.error">
|
||||
<h1 class="text-center my-4" v-text="playlist.name" />
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
@ -46,11 +46,12 @@
|
||||
width="168"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ErrorHandler from "./ErrorHandler.vue";
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
import VideoItem from "./VideoItem.vue";
|
||||
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
|
||||
|
||||
@ -59,6 +60,7 @@ export default {
|
||||
ErrorHandler,
|
||||
VideoItem,
|
||||
WatchOnYouTubeButton,
|
||||
LoadingIndicatorPage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -88,7 +90,7 @@ export default {
|
||||
},
|
||||
}).then(json => {
|
||||
if (json.error) alert(json.error);
|
||||
else if (json.filter(playlist => playlist.id === playlistId).length > 0) this.admin = true;
|
||||
else if (json.some(playlist => playlist.id === playlistId)) this.admin = true;
|
||||
});
|
||||
this.isPlaylistBookmarked();
|
||||
},
|
||||
|
@ -376,6 +376,7 @@ export default {
|
||||
languages: [
|
||||
{ code: "ar", name: "Arabic" },
|
||||
{ code: "az", name: "Azərbaycan" },
|
||||
{ code: "bg", name: "Български" },
|
||||
{ code: "bn", name: "বাংলা" },
|
||||
{ code: "bs", name: "Bosanski" },
|
||||
{ code: "ca", name: "Català" },
|
||||
@ -436,7 +437,7 @@ export default {
|
||||
|
||||
this.fetchJson("https://piped-instances.kavin.rocks/").then(resp => {
|
||||
this.instances = resp;
|
||||
if (this.instances.filter(instance => instance.api_url == this.apiUrl()).length == 0)
|
||||
if (!this.instances.some(instance => instance.api_url == this.apiUrl()))
|
||||
this.instances.push({
|
||||
name: "Custom Instance",
|
||||
api_url: this.apiUrl(),
|
||||
@ -615,4 +616,10 @@ export default {
|
||||
.pref {
|
||||
@apply flex justify-between items-center my-2 mx-[15vw] lt-md:mx-[2vw];
|
||||
}
|
||||
.pref:nth-child(odd) {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
.dark .pref:nth-child(odd) {
|
||||
@apply bg-dark-800;
|
||||
}
|
||||
</style>
|
||||
|
@ -18,19 +18,21 @@
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div v-if="results" class="video-grid">
|
||||
<LoadingIndicatorPage :show-content="results != null && results.items?.length" class="video-grid">
|
||||
<template v-for="result in results.items" :key="result.url">
|
||||
<ContentItem :item="result" height="94" width="168" />
|
||||
</template>
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentItem from "./ContentItem.vue";
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentItem,
|
||||
LoadingIndicatorPage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -3,17 +3,19 @@
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="video-grid">
|
||||
<LoadingIndicatorPage :show-content="videos.length != 0" class="video-grid">
|
||||
<VideoItem v-for="video in videos" :key="video.url" :item="video" height="118" width="210" />
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
import VideoItem from "./VideoItem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VideoItem,
|
||||
LoadingIndicatorPage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -12,10 +12,10 @@
|
||||
>
|
||||
<div class="w-full">
|
||||
<img
|
||||
class="w-full aspect-video"
|
||||
class="w-full aspect-video object-contain"
|
||||
:src="item.thumbnail"
|
||||
:alt="item.title"
|
||||
:class="{ 'shorts-img': item.isShort }"
|
||||
:class="{ 'shorts-img': item.isShort, 'opacity-75': item.watched }"
|
||||
loading="lazy"
|
||||
/>
|
||||
<!-- progress bar -->
|
||||
|
@ -92,7 +92,7 @@ export default {
|
||||
this.hotkeysPromise.then(() => {
|
||||
var self = this;
|
||||
this.$hotkeys(
|
||||
"f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+n,shift+,,shift+.,return",
|
||||
"f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+n,shift+,,shift+.,return,.,,",
|
||||
function (e, handler) {
|
||||
const videoEl = self.$refs.videoEl;
|
||||
switch (handler.key) {
|
||||
@ -191,6 +191,14 @@ export default {
|
||||
case "return":
|
||||
self.skipSegment(videoEl);
|
||||
break;
|
||||
case ".":
|
||||
videoEl.currentTime += 0.04;
|
||||
e.preventDefault();
|
||||
break;
|
||||
case ",":
|
||||
videoEl.currentTime -= 0.04;
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -206,6 +214,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async loadVideo() {
|
||||
this.updateSponsors();
|
||||
|
||||
const component = this;
|
||||
const videoEl = this.$refs.videoEl;
|
||||
|
||||
@ -263,9 +273,7 @@ export default {
|
||||
|
||||
const MseSupport = window.MediaSource !== undefined;
|
||||
|
||||
const lbry = this.getPreferenceBoolean("disableLBRY", false)
|
||||
? null
|
||||
: this.video.videoStreams.filter(stream => stream.quality === "LBRY")[0];
|
||||
const lbry = null;
|
||||
|
||||
var uri;
|
||||
var mime;
|
||||
@ -275,9 +283,10 @@ export default {
|
||||
mime = "application/x-mpegURL";
|
||||
} else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
|
||||
if (!this.video.dash) {
|
||||
const dash = (
|
||||
await import("@/utils/DashUtils.js").then(mod => mod.default)
|
||||
).generate_dash_file_from_formats(streams, this.video.duration);
|
||||
const dash = (await import("../utils/DashUtils.js")).generate_dash_file_from_formats(
|
||||
streams,
|
||||
this.video.duration,
|
||||
);
|
||||
|
||||
uri = "data:application/dash+xml;charset=utf-8;base64," + btoa(dash);
|
||||
} else {
|
||||
@ -313,7 +322,7 @@ export default {
|
||||
uri = this.video.hls;
|
||||
mime = "application/x-mpegURL";
|
||||
} else {
|
||||
uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url;
|
||||
uri = this.video.videoStreams.findLast(stream => stream.codec == null).url;
|
||||
mime = "video/mp4";
|
||||
}
|
||||
|
||||
@ -363,6 +372,9 @@ export default {
|
||||
else this.setPlayerAttrs(this.$player, videoEl, uri, mime, this.$shaka);
|
||||
|
||||
if (noPrevPlayer) {
|
||||
videoEl.addEventListener("loadeddata", () => {
|
||||
if (document.pictureInPictureElement) videoEl.requestPictureInPicture();
|
||||
});
|
||||
videoEl.addEventListener("timeupdate", () => {
|
||||
const time = videoEl.currentTime;
|
||||
this.$emit("timeupdate", time);
|
||||
@ -647,22 +659,7 @@ export default {
|
||||
|
||||
if (markers) markers.style.background = `linear-gradient(${array.join(",")})`;
|
||||
},
|
||||
destroy(hotkeys) {
|
||||
if (this.$ui) {
|
||||
this.$ui.destroy();
|
||||
this.$ui = undefined;
|
||||
this.$player = undefined;
|
||||
}
|
||||
if (this.$player) {
|
||||
this.$player.destroy();
|
||||
this.$player = undefined;
|
||||
}
|
||||
if (hotkeys) this.$hotkeys?.unbind();
|
||||
this.$refs.container?.querySelectorAll("div").forEach(node => node.remove());
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sponsors() {
|
||||
updateSponsors() {
|
||||
const skipOptions = this.getPreferenceJSON("skipOptions", {});
|
||||
this.sponsors?.segments?.forEach(segment => {
|
||||
const option = skipOptions[segment.category];
|
||||
@ -674,6 +671,19 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
destroy(hotkeys) {
|
||||
if (this.$ui && !document.pictureInPictureElement) {
|
||||
this.$ui.destroy();
|
||||
this.$ui = undefined;
|
||||
this.$player = undefined;
|
||||
}
|
||||
if (this.$player) {
|
||||
this.$player.destroy();
|
||||
if (!document.pictureInPictureElement) this.$player = undefined;
|
||||
}
|
||||
if (hotkeys) this.$hotkeys?.unbind();
|
||||
this.$refs.container?.querySelectorAll("div").forEach(node => node.remove());
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -10,7 +10,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="video && !isEmbed" class="w-full">
|
||||
<LoadingIndicatorPage :show-content="video && !isEmbed" class="w-full">
|
||||
<ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" />
|
||||
<Transition>
|
||||
<ToastComponent v-if="shouldShowToast" @dismissed="dismiss">
|
||||
@ -20,15 +20,17 @@
|
||||
|
||||
<div v-show="!video.error">
|
||||
<div :class="isMobile ? 'flex-col' : 'flex'">
|
||||
<VideoPlayer
|
||||
ref="videoPlayer"
|
||||
:video="video"
|
||||
:sponsors="sponsors"
|
||||
:selected-auto-play="selectedAutoPlay"
|
||||
:selected-auto-loop="selectedAutoLoop"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onVideoEnded"
|
||||
/>
|
||||
<keep-alive>
|
||||
<VideoPlayer
|
||||
ref="videoPlayer"
|
||||
:video="video"
|
||||
:sponsors="sponsors"
|
||||
:selected-auto-play="selectedAutoPlay"
|
||||
:selected-auto-loop="selectedAutoLoop"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onVideoEnded"
|
||||
/>
|
||||
</keep-alive>
|
||||
<ChaptersBar
|
||||
:mobileLayout="isMobile"
|
||||
v-if="video?.chapters?.length > 0 && showChapters"
|
||||
@ -219,7 +221,7 @@
|
||||
<hr class="sm:hidden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingIndicatorPage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -232,6 +234,7 @@ import PlaylistAddModal from "./PlaylistAddModal.vue";
|
||||
import ShareModal from "./ShareModal.vue";
|
||||
import PlaylistVideos from "./PlaylistVideos.vue";
|
||||
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
|
||||
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
|
||||
import ToastComponent from "./ToastComponent.vue";
|
||||
|
||||
export default {
|
||||
@ -246,14 +249,13 @@ export default {
|
||||
ShareModal,
|
||||
PlaylistVideos,
|
||||
WatchOnYouTubeButton,
|
||||
LoadingIndicatorPage,
|
||||
ToastComponent,
|
||||
},
|
||||
data() {
|
||||
const smallViewQuery = window.matchMedia("(max-width: 640px)");
|
||||
return {
|
||||
video: {
|
||||
title: "Loading...",
|
||||
},
|
||||
video: null,
|
||||
playlistId: null,
|
||||
playlist: null,
|
||||
index: null,
|
||||
@ -361,7 +363,7 @@ export default {
|
||||
this.showDesc = !this.getPreferenceBoolean("minimizeDescription", false);
|
||||
this.showRecs = !this.getPreferenceBoolean("minimizeRecommendations", false);
|
||||
this.showChapters = !this.getPreferenceBoolean("minimizeChapters", false);
|
||||
if (this.video.duration) {
|
||||
if (this.video?.duration) {
|
||||
document.title = this.video.title + " - Piped";
|
||||
this.$refs.videoPlayer.loadVideo();
|
||||
}
|
||||
|
@ -4,10 +4,10 @@
|
||||
"login": "تسجيل الدخول",
|
||||
"register": "إنشاء حساب",
|
||||
"preferences": "الإعدادات",
|
||||
"history": "تاريخ التصفح",
|
||||
"history": "سجل المشاهدة",
|
||||
"subscriptions": "الاشتراكات",
|
||||
"playlists": "قوائم التشغيل",
|
||||
"feed": "التغذية",
|
||||
"feed": "محتوى الاشتراكات",
|
||||
"account": "الحساب",
|
||||
"instance": "الخادم",
|
||||
"player": "المشغل",
|
||||
@ -127,7 +127,8 @@
|
||||
"skip_button_only": "إظهار زر التخطي",
|
||||
"skip_automatically": "تلقائيا",
|
||||
"min_segment_length": "الحد الأدنى لطول الفصل (بالثواني)",
|
||||
"skip_segment": "تخطي الجزء"
|
||||
"skip_segment": "تخطي الجزء",
|
||||
"show_less": "عرض أقل"
|
||||
},
|
||||
"video": {
|
||||
"sponsor_segments": "المقاطع الإعلانية",
|
||||
|
168
src/locales/bg.json
Normal file
168
src/locales/bg.json
Normal file
@ -0,0 +1,168 @@
|
||||
{
|
||||
"titles": {
|
||||
"channels": "Канали",
|
||||
"login": "Вход",
|
||||
"register": "Регистрация",
|
||||
"feed": "Абонаменти",
|
||||
"history": "История",
|
||||
"playlists": "Плейлисти",
|
||||
"instance": "Инстанция",
|
||||
"player": "Плейър",
|
||||
"livestreams": "Излъчвания на живо",
|
||||
"bookmarks": "Отметки",
|
||||
"trending": "Набиращи популярност",
|
||||
"account": "Профил",
|
||||
"preferences": "Настройки",
|
||||
"subscriptions": "Абонаменти"
|
||||
},
|
||||
"actions": {
|
||||
"most_recent": "Най-скорошен",
|
||||
"unsubscribe": "Отписване - {count}",
|
||||
"uses_api_from": "Използва API от ",
|
||||
"skip_sponsors": "Пропускане на спонсори",
|
||||
"skip_preview": "Пропускане на преглед/обобщение",
|
||||
"skip_self_promo": "Пропускане на самореклама/неплатена реклама",
|
||||
"min_segment_length": "Минимална дължина на сегмента (в секунди)",
|
||||
"default_quality": "Качество по подразбиране",
|
||||
"minimize_comments_default": "Минимизиране на коментарите по подразбиране",
|
||||
"subscribe": "Абониране - {count}",
|
||||
"view_subscriptions": "Преглед на абонаменти",
|
||||
"sort_by": "Сортиране по:",
|
||||
"least_recent": "Най-малко скорошен",
|
||||
"channel_name_asc": "Име на канал (А-Я)",
|
||||
"channel_name_desc": "Име на канал (Я-А)",
|
||||
"back": "Назад",
|
||||
"enable_sponsorblock": "Активиране на SponsorBlock",
|
||||
"skip_button_only": "Показване на бутона за пропускане",
|
||||
"skip_automatically": "Автоматично",
|
||||
"skip_intro": "Пропускане на прекъсване/въвеждаща анимация",
|
||||
"skip_outro": "Пропускане на крайни карти/надписи",
|
||||
"skip_interaction": "Пропускане на напомняне за абониране",
|
||||
"skip_non_music": "Попускане Немузикален раздел в музика",
|
||||
"skip_highlight": "Пропускане на видео акцент",
|
||||
"show_markers": "Показване на маркери в плейъра",
|
||||
"skip_segment": "Пропускане на сегмент",
|
||||
"theme": "Тема",
|
||||
"auto": "Автоматично",
|
||||
"dark": "Тъмна",
|
||||
"light": "Светла",
|
||||
"autoplay_video": "Автоматично пускане на видео",
|
||||
"audio_only": "Само аудио",
|
||||
"buffering_goal": "Буфериране (в секунди)",
|
||||
"country_selection": "Избор на държава",
|
||||
"default_homepage": "Начална страница по подразбиране",
|
||||
"minimize_description_default": "Минимизиране на описанието по подразбиране",
|
||||
"store_watch_history": "Запазване на историята на гледане",
|
||||
"language_selection": "Избор на език",
|
||||
"instances_list": "Списък на инстанциите",
|
||||
"enabled_codecs": "Разрешени кодеци (множество)",
|
||||
"instance_selection": "Избор на инстанция",
|
||||
"show_more": "Покажи повече",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
"export_to_json": "Експорт в JSON",
|
||||
"import_from_json": "Импорт от JSON/CSV",
|
||||
"loop_this_video": "Повтаряне на това видео",
|
||||
"auto_play_next_video": "Автоматично пускане на следващото видео",
|
||||
"donations": "Дарения за разработка",
|
||||
"minimize_comments": "Минимизиране на коментарите",
|
||||
"show_comments": "Показване на коментарите",
|
||||
"show_description": "Показване на описание",
|
||||
"search": "Търси",
|
||||
"minimize_description": "Минимизиране на описание",
|
||||
"filter": "Филтър",
|
||||
"clear_history": "Изчистване на историята",
|
||||
"minimize_recommendations": "Минимизиране на препоръчани",
|
||||
"show_recommendations": "Показване на препоръчани",
|
||||
"view_ssl_score": "Преглед на SSL резултат",
|
||||
"loading": "Зареждане...",
|
||||
"hide_replies": "Скрий отговорите",
|
||||
"load_more_replies": "Зареди още отговори",
|
||||
"remove_from_playlist": "Премахване от плейлист",
|
||||
"create_playlist": "Създаване на плейлист",
|
||||
"reset_preferences": "Нулиране на настройките",
|
||||
"with_timecode": "Сподели с текущото време",
|
||||
"piped_link": "Piped връзка",
|
||||
"documentation": "Документация",
|
||||
"delete_account": "Изтрий акаунта",
|
||||
"download_as_txt": "Изтегляне като .txt",
|
||||
"share": "Сподели",
|
||||
"follow_link": "Последвай връзката",
|
||||
"add_to_playlist": "Добави към плейлист",
|
||||
"delete_playlist_video_confirm": "Да се премахне ли видеото от плейлиста?",
|
||||
"show_watch_on_youtube": "Показване на бутона \"Гледай в YouTube\"",
|
||||
"source_code": "Изходен код",
|
||||
"minimize_chapters_default": "Минимизиране на разделите по подразбиране",
|
||||
"minimize_recommendations_default": "Минимизиране на препоръчани по подразбиране",
|
||||
"show_chapters": "Раздели",
|
||||
"logout": "Отписване от това устройство",
|
||||
"clone_playlist": "Клониране на плейлист",
|
||||
"clone_playlist_success": "Успешно клониране!",
|
||||
"backup_preferences": "Архивиране на настройките",
|
||||
"rename_playlist": "Преименуване на плейлиста",
|
||||
"new_playlist_name": "Ново име на плейлиста",
|
||||
"back_to_home": "Обратно към начална страница",
|
||||
"status_page": "Статус",
|
||||
"copy_link": "Копирай връзката",
|
||||
"time_code": "Текущо време (в секунди)",
|
||||
"reply_count": "{count} отговора",
|
||||
"restore_preferences": "Възстановяване на настройките",
|
||||
"invalidate_session": "Отписване от всички устройства",
|
||||
"different_auth_instance": "Използване на различна инстанция за удостоверяване",
|
||||
"store_search_history": "Запазване на историята на търсене",
|
||||
"instance_auth_selection": "Избор на инстанция за удостоверяване",
|
||||
"confirm_reset_preferences": "Сигурни ли сте, че искате да нулирате настройките?",
|
||||
"hide_watched": "Скриване на гледани видеоклипове в Абонаменти"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "Гледай в {0}"
|
||||
},
|
||||
"login": {
|
||||
"username": "Потребителско име",
|
||||
"password": "Парола"
|
||||
},
|
||||
"video": {
|
||||
"videos": "Видеоклипове",
|
||||
"views": "{views} показвания",
|
||||
"chapters": "Раздели",
|
||||
"all": "Всички",
|
||||
"watched": "Гледани",
|
||||
"category": "Категория"
|
||||
},
|
||||
"preferences": {
|
||||
"version": "Версия",
|
||||
"registered_users": "Регистрирани потребители",
|
||||
"instance_locations": "Местоположения на инстанция",
|
||||
"instance_name": "Име на инстанция",
|
||||
"has_cdn": "Има ли CDN?",
|
||||
"up_to_date": "Актуален?",
|
||||
"ssl_score": "SSL резултат"
|
||||
},
|
||||
"comment": {
|
||||
"disabled": "Коментарите са деактивирани.",
|
||||
"pinned_by": "Фиксиран от {author}",
|
||||
"loading": "Коментарите се зареждат...",
|
||||
"user_disabled": "Коментарите са деактивирани в настройките."
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "Имахте предвид: {0}?",
|
||||
"all": "YouTube: Всички",
|
||||
"videos": "YouTube: Видеоклипове",
|
||||
"channels": "YouTube: Канали",
|
||||
"playlists": "YouTube: Плейлисти",
|
||||
"music_songs": "YT Music: Песни",
|
||||
"music_videos": "YT Music: Видеоклипове",
|
||||
"music_albums": "YT Music: Албуми",
|
||||
"music_playlists": "YT Music: Плейлисти"
|
||||
},
|
||||
"subscriptions": {
|
||||
"subscribed_channels_count": "Абониран за: {0}"
|
||||
},
|
||||
"info": {
|
||||
"page_not_found": "Страницата не е намерена",
|
||||
"copied": "Копирано!",
|
||||
"cannot_copy": "Не може да се копира!",
|
||||
"local_storage": "Това действие изисква localStorage, разрешени ли са бисквитките?",
|
||||
"register_no_email_note": "Използването на имейл като потребителско име не се препоръчва. Продължете все пак?"
|
||||
}
|
||||
}
|
@ -104,7 +104,11 @@
|
||||
"show_watch_on_youtube": "Schaltfläche „Auf YouTube ansehen“ anzeigen",
|
||||
"with_playlist": "Mit Wiedergabeliste teilen",
|
||||
"playlist_bookmarked": "Markiert",
|
||||
"bookmark_playlist": "Lesezeichen"
|
||||
"bookmark_playlist": "Lesezeichen",
|
||||
"skip_segment": "Segment überspringen",
|
||||
"skip_automatically": "Automatisch",
|
||||
"min_segment_length": "Minimale Segmentlänge (in Sekunden)",
|
||||
"skip_button_only": "Überspringen-Schaltfläche anzeigen"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "Auf {0} ansehen"
|
||||
@ -134,7 +138,8 @@
|
||||
"live": "{0} Live",
|
||||
"chapters": "Kapitel",
|
||||
"shorts": "Shorts",
|
||||
"all": "Alle"
|
||||
"all": "Alle",
|
||||
"category": "Kategorie"
|
||||
},
|
||||
"preferences": {
|
||||
"ssl_score": "SSL-Bewertung",
|
||||
|
@ -129,7 +129,9 @@
|
||||
"with_playlist": "Share with playlist",
|
||||
"bookmark_playlist": "Bookmark",
|
||||
"playlist_bookmarked": "Bookmarked",
|
||||
"dismiss": "Dismiss"
|
||||
"dismiss": "Dismiss",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less"
|
||||
},
|
||||
"comment": {
|
||||
"pinned_by": "Pinned by {author}",
|
||||
|
@ -42,7 +42,7 @@
|
||||
"instance_selection": "Selección de instancias",
|
||||
"enabled_codecs": "Códecs habilitados (múltiples)",
|
||||
"instances_list": "Lista de instancias",
|
||||
"language_selection": "Selección de lenguajes",
|
||||
"language_selection": "Selección de idioma",
|
||||
"store_watch_history": "Recordar historial de visualización",
|
||||
"minimize_description_default": "Minimizar la descripción por defecto",
|
||||
"show_comments": "Mostrar comentarios",
|
||||
|
@ -120,7 +120,11 @@
|
||||
"no_valid_playlists": "Le fichier ne contient pas de listes de lecture valides !",
|
||||
"bookmark_playlist": "Marque-page",
|
||||
"playlist_bookmarked": "Dans les marque-pages",
|
||||
"with_playlist": "Partager avec la liste de lecture"
|
||||
"with_playlist": "Partager avec la liste de lecture",
|
||||
"skip_button_only": "Afficher le bouton de saut",
|
||||
"skip_automatically": "Automatiquement",
|
||||
"min_segment_length": "Longueur minimale du segment (en secondes)",
|
||||
"skip_segment": "Sauter le segment"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "Regarder sur {0}"
|
||||
@ -134,7 +138,8 @@
|
||||
"chapters": "Chapitres",
|
||||
"live": "{0} en direct",
|
||||
"shorts": "Courtes",
|
||||
"all": "Tout"
|
||||
"all": "Tout",
|
||||
"category": "Catégorie"
|
||||
},
|
||||
"preferences": {
|
||||
"ssl_score": "Score SSL",
|
||||
|
@ -53,7 +53,7 @@
|
||||
"instances_list": "רשימת עותקים",
|
||||
"enabled_codecs": "מפענחים פעילים (מגוון)",
|
||||
"instance_selection": "בחירת עותק",
|
||||
"show_more": "להציג עוד",
|
||||
"show_more": "להציג יותר",
|
||||
"yes": "כן",
|
||||
"no": "לא",
|
||||
"export_to_json": "ייצוא ל־JSON",
|
||||
@ -127,7 +127,8 @@
|
||||
"skip_button_only": "הצגת כפתור דילוג",
|
||||
"min_segment_length": "אורך מקטע מזערי (בשניות)",
|
||||
"skip_segment": "דילוג על מקטע",
|
||||
"skip_automatically": "אוטומטית"
|
||||
"skip_automatically": "אוטומטית",
|
||||
"show_less": "להציג פחות"
|
||||
},
|
||||
"comment": {
|
||||
"pinned_by": "ננעץ על ידי {author}",
|
||||
|
@ -8,7 +8,8 @@
|
||||
"chapters": "Poglavlja",
|
||||
"live": "{0} uživo",
|
||||
"shorts": "Kratka videa",
|
||||
"all": "Sva"
|
||||
"all": "Sva",
|
||||
"category": "Kategorija"
|
||||
},
|
||||
"preferences": {
|
||||
"ssl_score": "SSL ocjena",
|
||||
@ -92,7 +93,7 @@
|
||||
"select_playlist": "Odaberi popis snimaka",
|
||||
"please_select_playlist": "Odaberi popis snimaka",
|
||||
"delete_playlist_video_confirm": "Ukloniti video iz popisa snimaka?",
|
||||
"show_markers": "Prikaži oznake na Pokretaču",
|
||||
"show_markers": "Prikaži oznake na playeru",
|
||||
"delete_account": "Izbriši račun",
|
||||
"logout": "Odjavi se s ovog uređaja",
|
||||
"minimize_recommendations_default": "Standardno sakrij preporuke",
|
||||
@ -130,7 +131,11 @@
|
||||
"no_valid_playlists": "Datoteka ne sadrži ispravne popise snimaka!",
|
||||
"with_playlist": "Dijeli s popisom snimaka",
|
||||
"playlist_bookmarked": "Zabilježeno",
|
||||
"bookmark_playlist": "Zabilježi"
|
||||
"bookmark_playlist": "Zabilježi",
|
||||
"skip_button_only": "Prikaži gumb za preskakanje",
|
||||
"skip_automatically": "Automatski",
|
||||
"skip_segment": "Preskoči segment",
|
||||
"min_segment_length": "Najmanja duljina segmenta (u sekundama)"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "Gledaj na {0}"
|
||||
@ -146,7 +151,7 @@
|
||||
"playlists": "Popisi snimaka",
|
||||
"account": "Račun",
|
||||
"instance": "Instanca",
|
||||
"player": "Pokretač",
|
||||
"player": "Player",
|
||||
"channels": "Kanali",
|
||||
"livestreams": "Prijenosi uživo",
|
||||
"bookmarks": "Zabilješke"
|
||||
|
@ -138,7 +138,8 @@
|
||||
"live": "{0} Diretta",
|
||||
"chapters": "Capitoli",
|
||||
"shorts": "Short",
|
||||
"all": "Tutti"
|
||||
"all": "Tutti",
|
||||
"category": "Categoria"
|
||||
},
|
||||
"preferences": {
|
||||
"ssl_score": "Valutazione SSL",
|
||||
|
@ -29,13 +29,13 @@
|
||||
"channel_name_desc": "チャンネル名 (ZからA)",
|
||||
"back": "戻る",
|
||||
"uses_api_from": "API使用元 ",
|
||||
"enable_sponsorblock": "SponsorBlockを有効化",
|
||||
"enable_sponsorblock": "SponsorBlock を有効化",
|
||||
"skip_sponsors": "広告をスキップ",
|
||||
"skip_intro": "休止時間/イントロ画面をスキップ",
|
||||
"skip_outro": "終了画面/クレジットをスキップ",
|
||||
"skip_intro": "休止時間/導入アニメをスキップ",
|
||||
"skip_outro": "終了シーン/クレジットをスキップ",
|
||||
"skip_preview": "プレビュー/要約をスキップ",
|
||||
"skip_interaction": "チャンネル登録など操作を求める自己宣伝をスキップ",
|
||||
"skip_self_promo": "無償/自己プロモーションをスキップ",
|
||||
"skip_self_promo": "無報酬/自己の宣伝をスキップ",
|
||||
"skip_non_music": "音楽: 非音楽部分をスキップ",
|
||||
"theme": "テーマ",
|
||||
"auto": "自動",
|
||||
@ -43,12 +43,12 @@
|
||||
"light": "ライト",
|
||||
"autoplay_video": "動画を自動再生",
|
||||
"audio_only": "音声のみ",
|
||||
"default_quality": "デフォルトの画質",
|
||||
"default_quality": "標準の画質",
|
||||
"buffering_goal": "バッファリング目標値 (秒)",
|
||||
"country_selection": "国の選択",
|
||||
"default_homepage": "ホームに表示するページ",
|
||||
"show_comments": "コメントを表示",
|
||||
"minimize_description_default": "デフォルトで詳細を最小化する",
|
||||
"minimize_description_default": "最初から説明を最小化",
|
||||
"store_watch_history": "再生履歴を保存する",
|
||||
"language_selection": "言語の選択",
|
||||
"instances_list": "インスタンス一覧",
|
||||
@ -68,16 +68,16 @@
|
||||
"show_recommendations": "おすすめを見る",
|
||||
"disable_lbry": "ストリーミングのLBRYを無効化",
|
||||
"enable_lbry_proxy": "LBRYプロキシをオン",
|
||||
"view_ssl_score": "SSLスコアを見る",
|
||||
"view_ssl_score": "SSLの評価を表示",
|
||||
"search": "検索",
|
||||
"filter": "フィルター",
|
||||
"loading": "読み込み中…",
|
||||
"clear_history": "再生履歴を削除",
|
||||
"hide_replies": "返信を非表示",
|
||||
"load_more_replies": "返信をもっと見る",
|
||||
"skip_filler_tangent": "無関係なコンテンツをスキップ",
|
||||
"skip_filler_tangent": "無関係な談話をスキップ",
|
||||
"skip_highlight": "ハイライトをスキップ",
|
||||
"add_to_playlist": "再生リストに追加する",
|
||||
"add_to_playlist": "再生リストに追加",
|
||||
"create_playlist": "再生リストを作成",
|
||||
"remove_from_playlist": "再生リストから削除",
|
||||
"delete_playlist_video_confirm": "再生リストからこの動画を削除しますか?",
|
||||
@ -98,9 +98,9 @@
|
||||
"different_auth_instance": "認証に別のインスタンスを使う",
|
||||
"download_as_txt": ".txtでダウンロード",
|
||||
"logout": "このデバイスでログアウト",
|
||||
"minimize_recommendations_default": "デフォルトでおすすめを最小化する",
|
||||
"minimize_recommendations_default": "最初からおすすめを最小化",
|
||||
"hide_watched": "再生済みの動画をフィードに表示しない",
|
||||
"minimize_chapters_default": "デフォルトでチャプターを最小化する",
|
||||
"minimize_chapters_default": "最初からチャプターを最小化",
|
||||
"show_watch_on_youtube": "「YouTubeで見る」ボタンを表示する",
|
||||
"invalidate_session": "すべてのデバイスでログアウトする",
|
||||
"instance_auth_selection": "認証インスタンスの選択",
|
||||
@ -119,11 +119,15 @@
|
||||
"follow_link": "リンクに従う",
|
||||
"reply_count": "{count} 件の返信",
|
||||
"clone_playlist": "再生リストを複製",
|
||||
"minimize_comments_default": "デフォルトでコメントを最小化する",
|
||||
"no_valid_playlists": "ファイルに有効な再生リストが含まれていません。",
|
||||
"minimize_comments_default": "最初からコメントを最小化",
|
||||
"no_valid_playlists": "このファイルは有効な再生リストではありません!",
|
||||
"playlist_bookmarked": "ブックマーク完了",
|
||||
"bookmark_playlist": "ブックマーク",
|
||||
"with_playlist": "再生リストで共有"
|
||||
"with_playlist": "再生リストで共有",
|
||||
"skip_automatically": "自動",
|
||||
"skip_button_only": "スキップボタン表示",
|
||||
"skip_segment": "ここをスキップ",
|
||||
"min_segment_length": "最小の区切りの長さ (秒)"
|
||||
},
|
||||
"comment": {
|
||||
"pinned_by": "{author} によって固定",
|
||||
@ -135,8 +139,8 @@
|
||||
"instance_name": "インスタンス名",
|
||||
"instance_locations": "インスタンスの場所",
|
||||
"has_cdn": "CDNの有無",
|
||||
"ssl_score": "SSLスコア",
|
||||
"registered_users": "登録済みユーザー",
|
||||
"ssl_score": "SSLの評価",
|
||||
"registered_users": "登録ユーザー数",
|
||||
"version": "バージョン",
|
||||
"up_to_date": "最新?"
|
||||
},
|
||||
@ -153,7 +157,8 @@
|
||||
"chapters": "チャプター",
|
||||
"live": "{0} ライブ配信",
|
||||
"shorts": "ショート",
|
||||
"all": "すべて"
|
||||
"all": "すべて",
|
||||
"category": "分類"
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "もしかして: {0}?",
|
||||
|
@ -58,15 +58,15 @@
|
||||
"remove_from_playlist": "Uit Afspeellijst Verwijderen",
|
||||
"select_playlist": "Selecteer een Afspeellijst",
|
||||
"delete_playlist_confirm": "Deze afspeellijst verwijderen?",
|
||||
"please_select_playlist": "Kies een afspeellijst a.u.b.",
|
||||
"please_select_playlist": "Selecteer een afspeellijst alsjeblief",
|
||||
"instance_selection": "Instantie Selectie",
|
||||
"import_from_json": "Importeren uit JSON/CSV",
|
||||
"clear_history": "Geschiedenis Wissen",
|
||||
"load_more_replies": "Laad meer Antwoorden",
|
||||
"delete_playlist_video_confirm": "Video van playlist verwijderen?",
|
||||
"delete_playlist_video_confirm": "Video uit deze afspeellijst verwijderen?",
|
||||
"create_playlist": "Afspeellijst Maken",
|
||||
"delete_playlist": "Afspeellijst Verwijderen",
|
||||
"show_markers": "Toon markeringen op Speler",
|
||||
"show_markers": "Laat markeringen op speler zien",
|
||||
"store_search_history": "Zoekgeschiedenis Opslaan",
|
||||
"minimize_chapters_default": "Hoofdstukken Standaard Minimaliseren",
|
||||
"show_watch_on_youtube": "Toon Bekijk op YouTube knop",
|
||||
@ -77,8 +77,8 @@
|
||||
"copy_link": "Link kopiëren",
|
||||
"hide_watched": "Verberg bekeken video's in de feed",
|
||||
"minimize_comments": "Opmerkingen minimaliseren",
|
||||
"instance_auth_selection": "Autenticatie Instantie Selectie",
|
||||
"clone_playlist": "Afspeellijst klonen",
|
||||
"instance_auth_selection": "Selectie authenticatie-instantie",
|
||||
"clone_playlist": "Afspeellijst dupliceren",
|
||||
"download_as_txt": "Downloaden als .txt",
|
||||
"rename_playlist": "Afspeellijst hernoemen",
|
||||
"new_playlist_name": "Nieuwe afspeellijstnaam",
|
||||
@ -91,20 +91,24 @@
|
||||
"instance_donations": "Instantie donaties",
|
||||
"reply_count": "{count} antwoorden",
|
||||
"no_valid_playlists": "Het bestand bevat geen geldige afspeellijsten!",
|
||||
"clone_playlist_success": "Succesvol gekloond!",
|
||||
"reset_preferences": "Voorkeuren opnieuw instellen",
|
||||
"clone_playlist_success": "Dupliceren gelukt!",
|
||||
"reset_preferences": "Voorkeuren herstellen",
|
||||
"back_to_home": "Terug naar de start",
|
||||
"minimize_comments_default": "Opmerkingen Standaard Minimaliseren",
|
||||
"delete_account": "Account Verwijderen",
|
||||
"logout": "Uitloggen van dit apparaat",
|
||||
"logout": "Uitloggen op dit apparaat",
|
||||
"minimize_recommendations_default": "Aanbevelingen Standaard Minimaliseren",
|
||||
"confirm_reset_preferences": "Weet u zeker dat u uw voorkeuren opnieuw wilt instellen?",
|
||||
"backup_preferences": "Back-up voorkeuren",
|
||||
"invalidate_session": "Alle apparaten afmelden",
|
||||
"invalidate_session": "Uitloggen op alle apparaten",
|
||||
"different_auth_instance": "Gebruik een andere instantie voor authenticatie",
|
||||
"with_playlist": "Delen met afspeellijst",
|
||||
"playlist_bookmarked": "Bladwijzer gemaakt",
|
||||
"bookmark_playlist": "Bladwijzer"
|
||||
"bookmark_playlist": "Bladwijzer",
|
||||
"skip_automatically": "Automatisch",
|
||||
"skip_button_only": "toon de overslaan knop",
|
||||
"min_segment_length": "Minimale segmentlengte (in seconden)",
|
||||
"skip_segment": "segment overslaan"
|
||||
},
|
||||
"titles": {
|
||||
"register": "Registreren",
|
||||
@ -113,9 +117,9 @@
|
||||
"preferences": "Voorkeuren",
|
||||
"history": "Geschiedenis",
|
||||
"subscriptions": "Abonnementen",
|
||||
"trending": "Trending",
|
||||
"trending": "populair",
|
||||
"playlists": "Afspeellijsten",
|
||||
"account": "Account",
|
||||
"account": "profiel",
|
||||
"instance": "Instantie",
|
||||
"player": "Speler",
|
||||
"livestreams": "Livestreams",
|
||||
@ -148,7 +152,9 @@
|
||||
"sponsor_segments": "Sponsorsegmenten",
|
||||
"ratings_disabled": "Beoordelingen Uitgeschakeld",
|
||||
"live": "{0} Live",
|
||||
"shorts": "Shorts"
|
||||
"shorts": "Shorts",
|
||||
"category": "Categorie",
|
||||
"all": "Alle"
|
||||
},
|
||||
"preferences": {
|
||||
"has_cdn": "Heeft CDN?",
|
||||
@ -161,8 +167,8 @@
|
||||
},
|
||||
"comment": {
|
||||
"pinned_by": "Vastgemaakt door {author}",
|
||||
"user_disabled": "Reacties zijn uitgeschakeld in de instellingen.",
|
||||
"loading": "Reacties laden...",
|
||||
"user_disabled": "Opmerkingen zijn uitgeschakeld in de instellingen.",
|
||||
"loading": "Opmerkingen laden...",
|
||||
"disabled": "Reacties zijn uitgeschakeld door de uploader."
|
||||
},
|
||||
"info": {
|
||||
@ -170,7 +176,8 @@
|
||||
"copied": "Gekopieerd!",
|
||||
"cannot_copy": "Kan niet kopiëren!",
|
||||
"page_not_found": "Pagina niet gevonden",
|
||||
"local_storage": "Deze actie vereist lokale opslag, zijn cookies ingeschakeld?"
|
||||
"local_storage": "Deze actie vereist lokale opslag, zijn cookies ingeschakeld?",
|
||||
"register_no_email_note": "Een e-mailadres als gebruikersnaam gebruiken wordt afgeraden. Toch doorgaan?"
|
||||
},
|
||||
"subscriptions": {
|
||||
"subscribed_channels_count": "Geabonneerd op: {0}"
|
||||
|
@ -144,7 +144,8 @@
|
||||
"ratings_disabled": "ମୂଲ୍ୟାୟନ ଅକ୍ଷମ ହୋଇଛି",
|
||||
"chapters": "ଅଧ୍ୟାୟ ଗୁଡ଼ିକ",
|
||||
"live": "{0} ସିଧାପ୍ରସାରଣ",
|
||||
"all": "ସମସ୍ତ"
|
||||
"all": "ସମସ୍ତ",
|
||||
"category": "ବର୍ଗ"
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "ଆପଣ କହିବାକୁ ଚାହୁଁଛନ୍ତି କି: {0}?",
|
||||
|
@ -157,7 +157,8 @@
|
||||
"chapters": "Rozdziały",
|
||||
"live": "{0} Na żywo",
|
||||
"shorts": "Krótkie wideo",
|
||||
"all": "Wszystkie"
|
||||
"all": "Wszystkie",
|
||||
"category": "Kategoria"
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "Czy chodziło ci o: {0}?",
|
||||
|
@ -100,7 +100,15 @@
|
||||
"minimize_recommendations": "Ascunde Recomandări",
|
||||
"yes": "Da",
|
||||
"show_comments": "Arată Comentarii",
|
||||
"show_description": "Arată Descriere"
|
||||
"show_description": "Arată Descriere",
|
||||
"bookmark_playlist": "Marcaj",
|
||||
"no_valid_playlists": "Fișierul nu conține playlist-uri valide!",
|
||||
"skip_automatically": "Automat",
|
||||
"min_segment_length": "Lungimea minimă a segmentului (în secunde)",
|
||||
"skip_segment": "Sări segmentul",
|
||||
"skip_button_only": "Afișează butonul de săritură",
|
||||
"with_playlist": "Distribuie cu playlist",
|
||||
"playlist_bookmarked": "Marcat"
|
||||
},
|
||||
"preferences": {
|
||||
"ssl_score": "Scor SSL",
|
||||
@ -125,7 +133,9 @@
|
||||
"sponsor_segments": "Segmente Sponsori",
|
||||
"ratings_disabled": "Like-uri dezactivate",
|
||||
"live": "{0} Live",
|
||||
"videos": "Video-uri"
|
||||
"videos": "Video-uri",
|
||||
"category": "Categorie",
|
||||
"all": "Tot"
|
||||
},
|
||||
"login": {
|
||||
"username": "Nume User",
|
||||
@ -146,7 +156,9 @@
|
||||
"cannot_copy": "Nu se poate copia!",
|
||||
"preferences_note": "Sfat: preferințele sunt salvate in memoria locala a browserului tău. Ștergând datele browserului le ștergi si pe ele.",
|
||||
"page_not_found": "Pagină negăsită",
|
||||
"copied": "S-a copiat!"
|
||||
"copied": "S-a copiat!",
|
||||
"register_no_email_note": "Utilizarea unui e-mail ca nume de utilizator nu este recomandată. Continui oricum?",
|
||||
"local_storage": "Această acțiune necesită localStorage, sunt activate cookie-urile?"
|
||||
},
|
||||
"subscriptions": {
|
||||
"subscribed_channels_count": "Abonat la: {0}"
|
||||
@ -164,7 +176,8 @@
|
||||
"livestreams": "Live-uri",
|
||||
"channels": "Canale",
|
||||
"preferences": "Preferințe",
|
||||
"player": "Player"
|
||||
"player": "Player",
|
||||
"bookmarks": "Marcaje"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "Vezi pe {0}"
|
||||
|
@ -5,8 +5,8 @@
|
||||
"register": "Регистрация",
|
||||
"feed": "Подписки",
|
||||
"preferences": "Настройки",
|
||||
"history": "История просмотров",
|
||||
"subscriptions": "Ваши подписки",
|
||||
"history": "История",
|
||||
"subscriptions": "Подписки",
|
||||
"playlists": "Плейлисты",
|
||||
"account": "Аккаунт",
|
||||
"player": "Плеер",
|
||||
@ -28,7 +28,7 @@
|
||||
"channel_name_asc": "Имя канала (А-Я)",
|
||||
"channel_name_desc": "Имя канала (Я-А)",
|
||||
"back": "Назад",
|
||||
"uses_api_from": "Использовать API, предоставляемое ",
|
||||
"uses_api_from": "Использовать API ",
|
||||
"enable_sponsorblock": "Включить Sponsorblock",
|
||||
"skip_sponsors": "Пропускать спонсорскую рекламу",
|
||||
"skip_intro": "Пропускать заставку/интро",
|
||||
@ -67,7 +67,7 @@
|
||||
"minimize_recommendations": "Свернуть рекомендации",
|
||||
"show_recommendations": "Показать рекомендации",
|
||||
"disable_lbry": "Отключить LBRY для стриминга",
|
||||
"enable_lbry_proxy": "Проксировать видео с LBRY",
|
||||
"enable_lbry_proxy": "Проксировать видео для LBRY",
|
||||
"view_ssl_score": "Посмотреть настройки SSL",
|
||||
"search": "Поиск",
|
||||
"filter": "Фильтр",
|
||||
@ -85,42 +85,42 @@
|
||||
"select_playlist": "Выбрать плейлист",
|
||||
"delete_playlist_confirm": "Удалить этот плейлист?",
|
||||
"delete_playlist_video_confirm": "Удалить видео из плейлиста?",
|
||||
"show_markers": "Показать Mаркеры Hа Проигрывателе",
|
||||
"show_markers": "Показать маркеры на проигрывателе",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"logout": "Выйти из этого устройства",
|
||||
"download_as_txt": "Скачать как .txt",
|
||||
"minimize_recommendations_default": "Скрыть Рекомендации по умолчанию",
|
||||
"invalidate_session": "Выйти из всех устройств",
|
||||
"different_auth_instance": "Использовать другие средства аутентификации",
|
||||
"instance_auth_selection": "Выбор средств аутентификации",
|
||||
"different_auth_instance": "Использовать другое зеркало для аутентификации",
|
||||
"instance_auth_selection": "Выбор зеркала аутентификации",
|
||||
"clone_playlist": "Клонировать плейлист",
|
||||
"clone_playlist_success": "Клонирование прошло успешно!",
|
||||
"show_chapters": "Части",
|
||||
"clone_playlist_success": "Успешно клонировано!",
|
||||
"show_chapters": "Главы",
|
||||
"rename_playlist": "Переименовать плейлист",
|
||||
"new_playlist_name": "Новое название плейлиста",
|
||||
"share": "Поделиться",
|
||||
"with_timecode": "Поделиться с отметкой времени",
|
||||
"with_timecode": "Поделиться с таймкодом",
|
||||
"piped_link": "Ссылка Piped",
|
||||
"follow_link": "Ссылка подписки",
|
||||
"follow_link": "Перейти по ссылке",
|
||||
"copy_link": "Скопировать ссылку",
|
||||
"time_code": "Тайм-код (в секундах)",
|
||||
"time_code": "Таймкод (в секундах)",
|
||||
"reset_preferences": "Сбросить настройки",
|
||||
"confirm_reset_preferences": "Вы уверены, что хотите сбросить настройки?",
|
||||
"backup_preferences": "Бэкап настроек",
|
||||
"restore_preferences": "Восстановить настройки",
|
||||
"back_to_home": "Вернутся на главную",
|
||||
"back_to_home": "Назад на главную",
|
||||
"store_search_history": "Хранить историю поиска",
|
||||
"hide_watched": "Скрыть просмотренные видео в ленте",
|
||||
"status_page": "Статус",
|
||||
"source_code": "Исходный код",
|
||||
"documentation": "Пожертвования сервера",
|
||||
"instance_donations": "Пожертвования сервера",
|
||||
"documentation": "Документация",
|
||||
"instance_donations": "Пожертвования зеркала",
|
||||
"reply_count": "{count} ответов",
|
||||
"minimize_comments_default": "Сворачивать комментарии по умолчанию",
|
||||
"minimize_comments": "Свернуть комментарии",
|
||||
"show_watch_on_youtube": "Показать кнопку Смотреть на YouTube",
|
||||
"minimize_chapters_default": "Скрывать главы по умолчанию",
|
||||
"no_valid_playlists": "Файл не содержит действительных списков воспроизведения!",
|
||||
"no_valid_playlists": "Файл не содержит действующих плейлистов!",
|
||||
"with_playlist": "Поделиться с плейлистом",
|
||||
"bookmark_playlist": "Закладка",
|
||||
"playlist_bookmarked": "В закладках",
|
||||
@ -130,14 +130,14 @@
|
||||
"skip_segment": "Пропустить сегмент"
|
||||
},
|
||||
"comment": {
|
||||
"pinned_by": "Прикреплено пользователем {author}",
|
||||
"pinned_by": "Закреплено пользователем {author}",
|
||||
"loading": "Загрузка комментариев...",
|
||||
"user_disabled": "Комментарии отключены в настройках.",
|
||||
"disabled": "Коментарии отключены автором."
|
||||
"disabled": "Комментарии отключены автором."
|
||||
},
|
||||
"preferences": {
|
||||
"instance_name": "Название",
|
||||
"instance_locations": "Местоположение",
|
||||
"instance_name": "Имя зеркала",
|
||||
"instance_locations": "Местоположения зеркала",
|
||||
"has_cdn": "Имеется CDN?",
|
||||
"ssl_score": "Оценка настроек SSL",
|
||||
"registered_users": "Зарегистрировано пользователей",
|
||||
@ -145,7 +145,7 @@
|
||||
"up_to_date": "Версия актуальна?"
|
||||
},
|
||||
"login": {
|
||||
"username": "Аккаунт на Piped",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль"
|
||||
},
|
||||
"video": {
|
||||
@ -157,7 +157,8 @@
|
||||
"live": "{0} В эфире",
|
||||
"chapters": "Содержание",
|
||||
"shorts": "Shorts",
|
||||
"all": "Все"
|
||||
"all": "Все",
|
||||
"category": "Категория"
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "Может быть вы имели в виду: {0}?",
|
||||
@ -174,11 +175,11 @@
|
||||
"subscribed_channels_count": "Подписан на: {0}"
|
||||
},
|
||||
"info": {
|
||||
"preferences_note": "Примечание: настройки сохранены в локальном хранилище браузера. При удалении данных браузера они будут удалены.",
|
||||
"preferences_note": "Примечание: настройки сохранены в локальном хранилище браузера. Удаление данных вашего браузера сбросит их.",
|
||||
"copied": "Скопировано!",
|
||||
"cannot_copy": "Не получилось скопировать!",
|
||||
"cannot_copy": "Не удалось скопировать!",
|
||||
"page_not_found": "Страница не найдена",
|
||||
"local_storage": "Это действие требует локального хранилища (localStorage), разрешены ли файлы cookie?",
|
||||
"local_storage": "Это действие требует разрешения localStorage, включены ли cookie-файлы?",
|
||||
"register_no_email_note": "Использование электронной почты в качестве имени пользователя не рекомендуется. Продолжить?"
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +157,8 @@
|
||||
"shorts": "කෙටි වීඩියෝ",
|
||||
"ratings_disabled": "ශ්රේණිගත කිරීම් අබල කර ඇත",
|
||||
"live": "{0} සජීවී",
|
||||
"all": "සියල්ල"
|
||||
"all": "සියල්ල",
|
||||
"category": "කාණ්ඩය"
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "ඔබ අදහස් කළේ: {0}?",
|
||||
|
@ -45,7 +45,7 @@
|
||||
"export_to_json": "JSON Olarak Dışa Aktar",
|
||||
"no": "Hayır",
|
||||
"yes": "Evet",
|
||||
"show_more": "Daha Fazla Göster",
|
||||
"show_more": "Daha fazla göster",
|
||||
"instance_selection": "Örnek Seçimi",
|
||||
"loading": "Yükleniyor...",
|
||||
"filter": "Filtrele",
|
||||
@ -108,7 +108,8 @@
|
||||
"min_segment_length": "En Küçük Bölüm Uzunluğu (saniye cinsinden)",
|
||||
"skip_segment": "Bölümü Atla",
|
||||
"skip_button_only": "Atla düğmesini göster",
|
||||
"skip_automatically": "Otomatik olarak"
|
||||
"skip_automatically": "Otomatik olarak",
|
||||
"show_less": "Daha az göster"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "{0} Üzerinde İzle"
|
||||
|
@ -28,7 +28,7 @@
|
||||
"show_comments": "Hiển thị bình luận",
|
||||
"store_watch_history": "Lịch sử xem trên cửa hàng",
|
||||
"language_selection": "Lựa chọn ngôn ngữ",
|
||||
"instances_list": "Danh sách phiên bản",
|
||||
"instances_list": "Danh sách instance",
|
||||
"show_more": "Hiện thị nhiều hơn",
|
||||
"import_from_json": "Nhập từ JSON/CSV",
|
||||
"loop_this_video": "Lặp lại video này",
|
||||
@ -53,7 +53,7 @@
|
||||
"light": "Sáng",
|
||||
"audio_only": "Chỉ có âm thanh",
|
||||
"minimize_description_default": "Thu nhỏ mô tả theo mặc định",
|
||||
"instance_selection": "Lựa chọn phiên bản",
|
||||
"instance_selection": "Lựa chọn instance",
|
||||
"yes": "Có",
|
||||
"enabled_codecs": "Các codec được bật (Nhiều)",
|
||||
"export_to_json": "Xuất định dạng JSON",
|
||||
@ -78,7 +78,8 @@
|
||||
"minimize_comments": "Thu nhỏ bình luận",
|
||||
"reply_count": "{count} phản hồi",
|
||||
"status_page": "Trạng thái",
|
||||
"new_playlist_name": "Tên danh sách phát mới"
|
||||
"new_playlist_name": "Tên danh sách phát mới",
|
||||
"skip_automatically": "Tự động"
|
||||
},
|
||||
"titles": {
|
||||
"register": "Đăng ký",
|
||||
@ -92,7 +93,8 @@
|
||||
"account": "Tài khoản",
|
||||
"channels": "Kênh",
|
||||
"instance": "Instance",
|
||||
"player": "Trình phát video"
|
||||
"player": "Trình phát video",
|
||||
"livestreams": "Phát sóng trực tiếp"
|
||||
},
|
||||
"player": {
|
||||
"watch_on": "Xem trên {0}"
|
||||
@ -104,8 +106,8 @@
|
||||
"disabled": "Bình luận đã bị tắt bởi người đăng video."
|
||||
},
|
||||
"preferences": {
|
||||
"instance_name": "Tên phiên bản",
|
||||
"instance_locations": "Vị trí phiên bản",
|
||||
"instance_name": "Tên instance",
|
||||
"instance_locations": "Vị trí instance",
|
||||
"has_cdn": "Có CDN?",
|
||||
"registered_users": "Người dùng đã đăng ký",
|
||||
"version": "Phiên bản",
|
||||
@ -124,7 +126,8 @@
|
||||
"live": "{0} Trực tiếp",
|
||||
"chapters": "Chương",
|
||||
"videos": "Video",
|
||||
"shorts": "Shorts"
|
||||
"shorts": "Shorts",
|
||||
"all": "Tất cả"
|
||||
},
|
||||
"search": {
|
||||
"did_you_mean": "Ý của bạn là: {0}?",
|
||||
|
@ -44,7 +44,7 @@
|
||||
"least_recent": "最早的",
|
||||
"most_recent": "最新的",
|
||||
"sort_by": "排序:",
|
||||
"view_subscriptions": "查看订阅",
|
||||
"view_subscriptions": "查看订阅列表",
|
||||
"unsubscribe": "取消订阅 - {count}",
|
||||
"subscribe": "订阅 - {count}",
|
||||
"loading": "正在加载...",
|
||||
@ -108,7 +108,8 @@
|
||||
"skip_automatically": "自动",
|
||||
"min_segment_length": "最小分段长度(以秒为单位)",
|
||||
"skip_segment": "跳过分段",
|
||||
"skip_button_only": "显示跳过按钮"
|
||||
"skip_button_only": "显示跳过按钮",
|
||||
"show_less": "显示更少"
|
||||
},
|
||||
"video": {
|
||||
"sponsor_segments": "赞助商部分",
|
||||
@ -141,8 +142,8 @@
|
||||
"watch_on": "在 {0} 观看"
|
||||
},
|
||||
"titles": {
|
||||
"feed": "RSS 订阅源",
|
||||
"subscriptions": "订阅",
|
||||
"feed": "订阅流",
|
||||
"subscriptions": "订阅列表",
|
||||
"history": "历史",
|
||||
"preferences": "设置",
|
||||
"register": "注册",
|
||||
|
@ -27,7 +27,7 @@ const routes = [
|
||||
component: () => import("../components/PlaylistPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:path(v|w|embed|shorts|watch)/:v?",
|
||||
path: "/:path(v|w|embed|live|shorts|watch)/:v?",
|
||||
name: "WatchVideo",
|
||||
component: () => import("../components/WatchVideo.vue"),
|
||||
},
|
||||
@ -41,6 +41,11 @@ const routes = [
|
||||
name: "Channel",
|
||||
component: () => import("../components/ChannelPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/@:channelId",
|
||||
name: "Channel handle",
|
||||
component: () => import("../components/ChannelPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
|
@ -4,201 +4,204 @@ import { Buffer } from "buffer";
|
||||
window.Buffer = Buffer;
|
||||
import { json2xml } from "xml-js";
|
||||
|
||||
const DashUtils = {
|
||||
generate_dash_file_from_formats(VideoFormats, VideoLength) {
|
||||
const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength);
|
||||
return json2xml(generatedJSON);
|
||||
},
|
||||
generate_xmljs_json_from_data(VideoFormatArray, VideoLength) {
|
||||
const convertJSON = {
|
||||
declaration: {
|
||||
attributes: {
|
||||
version: "1.0",
|
||||
encoding: "utf-8",
|
||||
},
|
||||
export function generate_dash_file_from_formats(VideoFormats, VideoLength) {
|
||||
const generatedJSON = generate_xmljs_json_from_data(VideoFormats, VideoLength);
|
||||
return json2xml(generatedJSON);
|
||||
}
|
||||
|
||||
function generate_xmljs_json_from_data(VideoFormatArray, VideoLength) {
|
||||
const convertJSON = {
|
||||
declaration: {
|
||||
attributes: {
|
||||
version: "1.0",
|
||||
encoding: "utf-8",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "MPD",
|
||||
attributes: {
|
||||
xmlns: "urn:mpeg:dash:schema:mpd:2011",
|
||||
profiles: "urn:mpeg:dash:profile:full:2011",
|
||||
minBufferTime: "PT1.5S",
|
||||
type: "static",
|
||||
mediaPresentationDuration: `PT${VideoLength}S`,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Period",
|
||||
elements: this.generate_adaptation_set(VideoFormatArray),
|
||||
},
|
||||
],
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "MPD",
|
||||
attributes: {
|
||||
xmlns: "urn:mpeg:dash:schema:mpd:2011",
|
||||
profiles: "urn:mpeg:dash:profile:full:2011",
|
||||
minBufferTime: "PT1.5S",
|
||||
type: "static",
|
||||
mediaPresentationDuration: `PT${VideoLength}S`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return convertJSON;
|
||||
},
|
||||
generate_adaptation_set(VideoFormatArray) {
|
||||
const adaptationSets = [];
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Period",
|
||||
elements: generate_adaptation_set(VideoFormatArray),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return convertJSON;
|
||||
}
|
||||
|
||||
let mimeAudioObjs = [];
|
||||
function generate_adaptation_set(VideoFormatArray) {
|
||||
const adaptationSets = [];
|
||||
|
||||
VideoFormatArray.forEach(videoFormat => {
|
||||
// the dual formats should not be used
|
||||
if (videoFormat.mimeType.indexOf("video") != -1 && !videoFormat.videoOnly) {
|
||||
let mimeAudioObjs = [];
|
||||
|
||||
VideoFormatArray.forEach(videoFormat => {
|
||||
// the dual formats should not be used
|
||||
if (
|
||||
(videoFormat.mimeType.includes("video") && !videoFormat.videoOnly) ||
|
||||
videoFormat.mimeType.includes("application")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const audioTrackId = videoFormat.audioTrackId;
|
||||
const mimeType = videoFormat.mimeType;
|
||||
|
||||
for (let i = 0; i < mimeAudioObjs.length; i++) {
|
||||
const mimeAudioObj = mimeAudioObjs[i];
|
||||
|
||||
if (mimeAudioObj.audioTrackId == audioTrackId && mimeAudioObj.mimeType == mimeType) {
|
||||
mimeAudioObj.videoFormats.push(videoFormat);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const audioTrackId = videoFormat.audioTrackId;
|
||||
const mimeType = videoFormat.mimeType;
|
||||
|
||||
for (let i = 0; i < mimeAudioObjs.length; i++) {
|
||||
const mimeAudioObj = mimeAudioObjs[i];
|
||||
|
||||
if (mimeAudioObj.audioTrackId == audioTrackId && mimeAudioObj.mimeType == mimeType) {
|
||||
mimeAudioObj.videoFormats.push(videoFormat);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mimeAudioObjs.push({
|
||||
audioTrackId,
|
||||
mimeType,
|
||||
videoFormats: [videoFormat],
|
||||
});
|
||||
mimeAudioObjs.push({
|
||||
audioTrackId,
|
||||
mimeType,
|
||||
videoFormats: [videoFormat],
|
||||
});
|
||||
});
|
||||
|
||||
mimeAudioObjs.forEach(mimeAudioObj => {
|
||||
const adapSet = {
|
||||
type: "element",
|
||||
name: "AdaptationSet",
|
||||
attributes: {
|
||||
id: mimeAudioObj.audioTrackId,
|
||||
lang: mimeAudioObj.audioTrackId?.substr(0, 2),
|
||||
mimeType: mimeAudioObj.mimeType,
|
||||
startWithSAP: "1",
|
||||
subsegmentAlignment: "true",
|
||||
},
|
||||
elements: [],
|
||||
};
|
||||
mimeAudioObjs.forEach(mimeAudioObj => {
|
||||
const adapSet = {
|
||||
type: "element",
|
||||
name: "AdaptationSet",
|
||||
attributes: {
|
||||
id: mimeAudioObj.audioTrackId,
|
||||
lang: mimeAudioObj.audioTrackId?.substr(0, 2),
|
||||
mimeType: mimeAudioObj.mimeType,
|
||||
startWithSAP: "1",
|
||||
subsegmentAlignment: "true",
|
||||
},
|
||||
elements: [],
|
||||
};
|
||||
|
||||
let isVideoFormat = false;
|
||||
let isVideoFormat = false;
|
||||
|
||||
if (mimeAudioObj.mimeType.includes("video")) {
|
||||
isVideoFormat = true;
|
||||
}
|
||||
if (mimeAudioObj.mimeType.includes("video")) {
|
||||
isVideoFormat = true;
|
||||
}
|
||||
|
||||
if (isVideoFormat) {
|
||||
adapSet.attributes.scanType = "progressive";
|
||||
}
|
||||
|
||||
for (var i = 0; i < mimeAudioObj.videoFormats.length; i++) {
|
||||
const videoFormat = mimeAudioObj.videoFormats[i];
|
||||
if (isVideoFormat) {
|
||||
adapSet.attributes.scanType = "progressive";
|
||||
adapSet.elements.push(generate_representation_video(videoFormat));
|
||||
} else {
|
||||
adapSet.elements.push(generate_representation_audio(videoFormat));
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < mimeAudioObj.videoFormats.length; i++) {
|
||||
const videoFormat = mimeAudioObj.videoFormats[i];
|
||||
if (isVideoFormat) {
|
||||
adapSet.elements.push(this.generate_representation_video(videoFormat));
|
||||
} else {
|
||||
adapSet.elements.push(this.generate_representation_audio(videoFormat));
|
||||
}
|
||||
}
|
||||
adaptationSets.push(adapSet);
|
||||
});
|
||||
return adaptationSets;
|
||||
}
|
||||
|
||||
adaptationSets.push(adapSet);
|
||||
});
|
||||
return adaptationSets;
|
||||
},
|
||||
generate_representation_audio(Format) {
|
||||
const representation = {
|
||||
type: "element",
|
||||
name: "Representation",
|
||||
attributes: {
|
||||
id: Format.itag,
|
||||
codecs: Format.codec,
|
||||
bandwidth: Format.bitrate,
|
||||
function generate_representation_audio(Format) {
|
||||
const representation = {
|
||||
type: "element",
|
||||
name: "Representation",
|
||||
attributes: {
|
||||
id: Format.itag,
|
||||
codecs: Format.codec,
|
||||
bandwidth: Format.bitrate,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "AudioChannelConfiguration",
|
||||
attributes: {
|
||||
schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
||||
value: "2",
|
||||
},
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "AudioChannelConfiguration",
|
||||
attributes: {
|
||||
schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
||||
value: "2",
|
||||
{
|
||||
type: "element",
|
||||
name: "BaseURL",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
text: Format.url,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "BaseURL",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
text: Format.url,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "SegmentBase",
|
||||
attributes: {
|
||||
indexRange: `${Format.indexStart}-${Format.indexEnd}`,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Initialization",
|
||||
attributes: {
|
||||
range: `${Format.initStart}-${Format.initEnd}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return representation;
|
||||
},
|
||||
generate_representation_video(Format) {
|
||||
const representation = {
|
||||
type: "element",
|
||||
name: "Representation",
|
||||
attributes: {
|
||||
id: Format.itag,
|
||||
codecs: Format.codec,
|
||||
bandwidth: Format.bitrate,
|
||||
width: Format.width,
|
||||
height: Format.height,
|
||||
maxPlayoutRate: "1",
|
||||
frameRate: Format.fps,
|
||||
],
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "BaseURL",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
text: Format.url,
|
||||
},
|
||||
],
|
||||
{
|
||||
type: "element",
|
||||
name: "SegmentBase",
|
||||
attributes: {
|
||||
indexRange: `${Format.indexStart}-${Format.indexEnd}`,
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "SegmentBase",
|
||||
attributes: {
|
||||
indexRange: `${Format.indexStart}-${Format.indexEnd}`,
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Initialization",
|
||||
attributes: {
|
||||
range: `${Format.initStart}-${Format.initEnd}`,
|
||||
},
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Initialization",
|
||||
attributes: {
|
||||
range: `${Format.initStart}-${Format.initEnd}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return representation;
|
||||
},
|
||||
};
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return representation;
|
||||
}
|
||||
|
||||
export default DashUtils;
|
||||
function generate_representation_video(Format) {
|
||||
const representation = {
|
||||
type: "element",
|
||||
name: "Representation",
|
||||
attributes: {
|
||||
id: Format.itag,
|
||||
codecs: Format.codec,
|
||||
bandwidth: Format.bitrate,
|
||||
width: Format.width,
|
||||
height: Format.height,
|
||||
maxPlayoutRate: "1",
|
||||
frameRate: Format.fps,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "BaseURL",
|
||||
elements: [
|
||||
{
|
||||
type: "text",
|
||||
text: Format.url,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "SegmentBase",
|
||||
attributes: {
|
||||
indexRange: `${Format.indexStart}-${Format.indexEnd}`,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Initialization",
|
||||
attributes: {
|
||||
range: `${Format.initStart}-${Format.initEnd}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return representation;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user