Skip to content
This repository has been archived by the owner on Nov 19, 2021. It is now read-only.

Commit

Permalink
Add live stream page
Browse files Browse the repository at this point in the history
  • Loading branch information
Allypost committed May 13, 2021
1 parent 3995b14 commit 4fe250b
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 4 deletions.
31 changes: 31 additions & 0 deletions api/routes/external/info/youtube/live.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
Router,
} from "../../../../helpers/route";
import {
getSetting,
} from "../../../../helpers/settings";
import YoutubeService from "../../../../services/external/google/youtube-service";

const router = new Router();

router.get("/video-id", async (): Promise<string | null> => {
const channelId: string = await getSetting("YouTube channel id");

if (!channelId) {
return null;
}

return await YoutubeService.getChannelLiveVideoId(channelId);
});

router.get("/is-live", async (): Promise<boolean> => {
const channelId: string = await getSetting("YouTube channel id");

if (!channelId) {
return false;
}

return await YoutubeService.isChannelLive(channelId);
});

export default router;
17 changes: 15 additions & 2 deletions api/routes/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import {
AuthRouter,
Router,
} from "../helpers/route";
import {
getSetting,
} from "../helpers/settings";
import YoutubeService from "../services/external/google/youtube-service";

const router = new Router();

router.get("/list", () => {
router.get("/list", async () => {
const youtubeChannelId = await getSetting("YouTube channel id", "");
const isYoutubeLive = await YoutubeService.isChannelLive(youtubeChannelId);

return [
{
name: "page.name.blog",
Expand All @@ -23,6 +30,12 @@ router.get("/list", () => {
to: { name: "PageSudionici" },
showOffline: false,
},
isYoutubeLive
? {
name: "page.name.broadcast",
to: { name: "PageLive" },
}
: null,
{
name: "page.name.contact",
to: { name: "PageKontakt" },
Expand All @@ -35,7 +48,7 @@ router.get("/list", () => {
// name: "button.joinNow",
// setting: "Join Now URL",
// },
].map((p) => ({
].filter((i) => i).map((p) => ({
showOffline: true,
...p,
}));
Expand Down
57 changes: 57 additions & 0 deletions api/services/external/google/youtube-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import xml2js from "xml2js";
import {
get,
} from "../../../helpers/axios";
import {
cachedFetcher,
CacheKey,
} from "../../../helpers/fetchCache";

const fetchChannelLiveInfo = cachedFetcher<null | string>(
"youtube-live",
15 * 1000,
async (channelId: string) => {
const page = await get<string>(
`https://www.youtube.com/channel/${ channelId }/live`,
{
validateStatus: (status) => 404 === status || 200 === status,
},
);

if (!page.includes("<meta name=\"title\"")) {
return null;
}

const xml = await get<string>(
`https://www.youtube.com/feeds/videos.xml?channel_id=${ channelId }`,
);

const data = await xml2js.parseStringPromise(
xml,
{
trim: true,
normalize: true,
normalizeTags: true,
},
);

return data?.feed?.entry?.shift()?.id?.pop()?.split(":")?.pop() || null;
},
(channelId: string) => channelId as CacheKey,
);

export default class YoutubeService {
public static async isChannelLive(channelId: string): Promise<boolean> {
const videoId = await this.getChannelLiveVideoId(channelId);

return null !== videoId;
}

public static async getChannelLiveVideoId(channelId: string): Promise<string | null> {
try {
return await fetchChannelLiveInfo(channelId);
} catch {
return null;
}
}
}
43 changes: 43 additions & 0 deletions components/DataRefresher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div style="display: none;" />
</template>

<script>
export default {
data: () => ({
timers: {},
}),
beforeDestroy() {
this.removeTimers();
},
mounted() {
this.addTimer(
"youtubeLiveCheck",
() => this.$store.dispatch("external/live/fetchYoutubeLiveVideoId"),
14000 + 2000 * Math.random(),
);
},
methods: {
addTimer(key, fn, timeout = 10000) {
this.$set(
this.timers,
key,
{
timeout,
fn,
timer: setInterval(() => fn(), timeout),
},
);
},
removeTimers() {
for (const { timer } of Object.values(this.timers)) {
clearInterval(timer);
}
},
},
};
</script>
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@nuxtjs/sentry": "^4.3.5",
"@sentry/node": "^5.24.2",
"@types/pg": "^7.14.11",
"@types/xml2js": "^0.4.8",
"adm-zip": "^0.4.16",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
Expand Down Expand Up @@ -70,7 +71,8 @@
"vue-router": "^3.1.6",
"vue-typer": "^1.2.0",
"vuelidate": "^0.7.6",
"vuex": "^3.3.0"
"vuex": "^3.3.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@babel/eslint-parser": "^7.12.1",
Expand Down
91 changes: 91 additions & 0 deletions pages/live.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<app-max-width-container>
<v-row>
<v-col cols="12">
<h1
:class="$style.header"
class="text-center my-12"
>
<translated-text trans-key="live.header" />
</h1>
</v-col>
</v-row>

<v-row>
<v-col cols="12">
<div :class="$style.wrapper">
<iframe
:class="$style.iframe"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
frameborder="0"
height="100%"
:src="`https://www.youtube.com/embed/${youtubeId}`"
title="YouTube video player"
width="100%"
/>
</div>
</v-col>
</v-row>
</app-max-width-container>
</template>

<router>
name: PageLive
</router>

<script>
import {
mapGetters,
} from "vuex";
import AppMaxWidthContainer from "../components/AppMaxWidthContainer";
import TranslatedText from "../components/TranslatedText";
export default {
components: {
TranslatedText,
AppMaxWidthContainer,
},
async validate({ store, redirect }) {
const isLive = await store.getters["external/live/isLive"];
if (isLive) {
return true;
}
return redirect({ name: "Index" });
},
computed: {
...mapGetters("external/live", [
"youtubeId",
]),
},
};
</script>

<style lang="scss" module>
@import "assets/styles/include/all";
.header {
font-size: 250%;
font-weight: bold;
text-align: center;
text-transform: uppercase;
color: $fer-dark-blue;
}
.wrapper {
position: relative;
padding-top: 56.25%;
.iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
</style>
41 changes: 41 additions & 0 deletions store/external/live.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Vue from "vue";

export const state = () => ({
youtubeLiveVideoId: null,
});

export const getters = {
youtubeId(state) {
return state.youtubeLiveVideoId;
},

isLive(state, getters) {
return Boolean(getters.youtubeId);
},
};

export const mutations = {
SET_YOUTUBE_ID(state, id) {
Vue.set(state, "youtubeLiveVideoId", id);
},
};

export const actions = {
async fetchYoutubeLiveVideoId({ commit }) {
const { data } = await this.$api.$get(
"/external/info/youtube/live/video-id",
{
progress: false,
},
);

commit("SET_YOUTUBE_ID", data);

return data;
},

async nuxtServerInit({ dispatch }) {
await dispatch("fetchYoutubeLiveVideoId");
},
};

22 changes: 21 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1941,6 +1941,13 @@
"@types/webpack-sources" "*"
source-map "^0.6.0"

"@types/xml2js@^0.4.8":
version "0.4.8"
resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a"
integrity sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA==
dependencies:
"@types/node" "*"

"@typescript-eslint/eslint-plugin@^4.21.0":
version "4.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz#3fce2bfa76d95c00ac4f33dff369cb593aab8878"
Expand Down Expand Up @@ -10305,7 +10312,7 @@ sass@^1.26.5:
dependencies:
chokidar ">=2.0.0 <4.0.0"

sax@>=0.6, sax@~1.2.4:
sax@>=0.6, sax@>=0.6.0, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
Expand Down Expand Up @@ -12463,6 +12470,19 @@ xml-crypto@^2.0.0:
xmldom "0.1.27"
xpath "0.0.27"

xml2js@^0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
dependencies:
sax ">=0.6.0"
xmlbuilder "~11.0.0"

xmlbuilder@~11.0.0:
version "11.0.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==

[email protected]:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
Expand Down

0 comments on commit 4fe250b

Please sign in to comment.