Merge remote-tracking branch 'origin/master' into toast

This commit is contained in:
Kavin 2023-03-17 04:13:04 +00:00
commit defefed9ed
No known key found for this signature in database
GPG Key ID: 49451E4482CC5BCD
37 changed files with 1176 additions and 729 deletions

View File

@ -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

View File

@ -2,6 +2,7 @@ server {
listen 80;
listen [::]:80;
server_name localhost;
error_log off;
location / {
root /usr/share/nginx/html;

View File

@ -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" />

View File

@ -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"
},

View File

@ -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");

View File

@ -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() {

View File

@ -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",

View 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>

View File

@ -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();
},

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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 -->

View File

@ -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>

View File

@ -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();
}

View File

@ -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
View 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": "Използването на имейл като потребителско име не се препоръчва. Продължете все пак?"
}
}

View File

@ -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",

View File

@ -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}",

View File

@ -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",

View File

@ -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",

View File

@ -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}",

View File

@ -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"

View File

@ -138,7 +138,8 @@
"live": "{0} Diretta",
"chapters": "Capitoli",
"shorts": "Short",
"all": "Tutti"
"all": "Tutti",
"category": "Categoria"
},
"preferences": {
"ssl_score": "Valutazione SSL",

View File

@ -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}",

View File

@ -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}"

View File

@ -144,7 +144,8 @@
"ratings_disabled": "ମୂଲ୍ୟାୟନ ଅକ୍ଷମ ହୋଇଛି",
"chapters": "ଅଧ୍ୟାୟ ଗୁଡ଼ିକ",
"live": "{0} ସିଧାପ୍ରସାରଣ",
"all": "ସମସ୍ତ"
"all": "ସମସ୍ତ",
"category": "ବର୍ଗ"
},
"search": {
"did_you_mean": "ଆପଣ କହିବାକୁ ଚାହୁଁଛନ୍ତି କି: {0}?",

View File

@ -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}?",

View File

@ -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}"

View File

@ -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": "Использование электронной почты в качестве имени пользователя не рекомендуется. Продолжить?"
}
}

View File

@ -157,7 +157,8 @@
"shorts": "කෙටි වීඩියෝ",
"ratings_disabled": "ශ්‍රේණිගත කිරීම් අබල කර ඇත",
"live": "{0} සජීවී",
"all": "සියල්ල"
"all": "සියල්ල",
"category": "කාණ්ඩය"
},
"search": {
"did_you_mean": "ඔබ අදහස් කළේ: {0}?",

View File

@ -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"

View File

@ -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}?",

View File

@ -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": "注册",

View File

@ -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",

View File

@ -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;
}

799
yarn.lock

File diff suppressed because it is too large Load Diff