Youtube: Video page + improved search

This commit is contained in:
pluja 2020-07-31 12:28:52 +02:00
parent f717f9bc91
commit 40a01beb58
43 changed files with 301 additions and 206 deletions

View File

@ -1 +1 @@
{"twitter": [{"username": "aantonop"}, {"username": "Trezor"}, {"username": "aitor13023985"}, {"username": "guaridadelzorro"}, {"username": "elonmusk"}, {"username": "bitcoin"}, {"username": "monero"}, {"username": "xmroutreach"}, {"username": "Locha_io"}, {"username": "randybrito"}, {"username": "btcven"}, {"username": "Snowden"}, {"username": "_prestwich"}, {"username": "TradeOnFire"}], "youtube": [{"channelId": "UCjr2bPAyPV7t35MvcgT3W8Q"}, {"channelId": "UCJWCJCWOxBYSi5DhCieLOLQ"}, {"channelId": "UCW3iqZr2cQFYKdO9Kpa97Yw"}, {"channelId": "UCLXo7UDZvByw2ixzpQCufnA"}, {"channelId": "UCR9sFzaG9Ia_kXJhfxtFMBA"}, {"channelId": "UCbdSYaPD-lr1kW27UJuk8Pw"}, {"channelId": "UCa3DVlGH2_QhvwuWlPa6MDQ"}, {"channelId": "UComoqWnjQlH7JpvqfyGnWNA"}, {"channelId": "UCA58hhLv4pdgDjBW5oNSklA"}, {"channelId": "UCkHR9m-tscD3ojD7_viIfTA"}, {"channelId": "UCYO_jab_esuFRV4b17AJtAw"}, {"channelId": "UC9-y-6csu5WGm29I7JiwpnA"}, {"channelId": "UC2PA-AKmVpU6NKCGtZq_rKQ"}, {"channelId": "UC2Qc22WUjL_GbBDIPvmHWmQ"}, {"channelId": "UC3Hx81QYLoEQkm3vyl4N4eQ"}, {"channelId": "UCiwy9Lx6h1Oi-UYwYDbgBZA"}, {"channelId": "UCHW_zn-nX5nvMjEA6mtNZ1A"}]} {"twitter": [{"username": "aantonop"}, {"username": "Trezor"}, {"username": "aitor13023985"}, {"username": "guaridadelzorro"}, {"username": "elonmusk"}, {"username": "bitcoin"}, {"username": "monero"}, {"username": "xmroutreach"}, {"username": "Locha_io"}, {"username": "randybrito"}, {"username": "btcven"}, {"username": "Snowden"}, {"username": "_prestwich"}, {"username": "TradeOnFire"}, {"username": "fluffypony"}, {"username": "browseyourlife"}], "youtube": [{"channelId": "UCjr2bPAyPV7t35MvcgT3W8Q"}, {"channelId": "UCJWCJCWOxBYSi5DhCieLOLQ"}, {"channelId": "UCW3iqZr2cQFYKdO9Kpa97Yw"}, {"channelId": "UCLXo7UDZvByw2ixzpQCufnA"}, {"channelId": "UCR9sFzaG9Ia_kXJhfxtFMBA"}, {"channelId": "UCbdSYaPD-lr1kW27UJuk8Pw"}, {"channelId": "UCa3DVlGH2_QhvwuWlPa6MDQ"}, {"channelId": "UComoqWnjQlH7JpvqfyGnWNA"}, {"channelId": "UCA58hhLv4pdgDjBW5oNSklA"}, {"channelId": "UCkHR9m-tscD3ojD7_viIfTA"}, {"channelId": "UCYO_jab_esuFRV4b17AJtAw"}, {"channelId": "UC9-y-6csu5WGm29I7JiwpnA"}, {"channelId": "UC2PA-AKmVpU6NKCGtZq_rKQ"}, {"channelId": "UC2Qc22WUjL_GbBDIPvmHWmQ"}, {"channelId": "UC3Hx81QYLoEQkm3vyl4N4eQ"}, {"channelId": "UCiwy9Lx6h1Oi-UYwYDbgBZA"}, {"channelId": "UCHW_zn-nX5nvMjEA6mtNZ1A"}, {"channelId": "UCy5znSnfMsDwaLlROnZ7Qbg"}, {"channelId": "UCJQQVLyM6wtPleV4wFBK06g"}, {"channelId": "UCyoAJbq6rqi9bgMB0uDQSFg"}, {"channelId": "UCsXVk37bltHxD1rDPwtNM8Q"}]}

View File

@ -102,6 +102,7 @@ class invidiousPost():
description = "LOREM IPSUM" description = "LOREM IPSUM"
date = 'None' date = 'None'
views = 'NaN' views = 'NaN'
id = 'isod'
class invidiousFollow(db.Model): class invidiousFollow(db.Model):

View File

@ -1,4 +1,4 @@
from flask import render_template, flash, redirect, url_for, request, send_from_directory from flask import render_template, flash, redirect, url_for, request, send_from_directory, Markup
from app.forms import LoginForm, RegistrationForm, EmptyForm, SearchForm, ChannelForm from app.forms import LoginForm, RegistrationForm, EmptyForm, SearchForm, ChannelForm
from app.models import User, twitterPost, invidiousPost, Post, invidiousFollow from app.models import User, twitterPost, invidiousPost, Post, invidiousFollow
from flask_login import login_user, logout_user, current_user, login_required from flask_login import login_user, logout_user, current_user, login_required
@ -6,23 +6,27 @@ from requests_futures.sessions import FuturesSession
from concurrent.futures import as_completed from concurrent.futures import as_completed
from werkzeug.urls import url_parse from werkzeug.urls import url_parse
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from flask import Markup
from app import app, db from app import app, db
import time, datetime
import random, string import random, string
import time, datetime
import feedparser import feedparser
import requests import requests
import json import json
import re import re
# Instances - Format must be instance.tld (No '/' and no 'https://')
nitterInstance = "https://nitter.net/" nitterInstance = "https://nitter.net/"
nitterInstanceII = "https://nitter.mastodont.cat" nitterInstanceII = "https://nitter.mastodont.cat/"
invidiousInstance = "invidious.snopyta.org"
#########################
#### Twitter Logic ######
#########################
@app.route('/') @app.route('/')
@app.route('/index') @app.route('/index')
@login_required @login_required
def index(): def index():
#start_time = time.time() start_time = time.time()
following = current_user.following_list() following = current_user.following_list()
followed = current_user.followed.count() followed = current_user.followed.count()
posts = [] posts = []
@ -34,141 +38,9 @@ def index():
profilePic = avatarPath profilePic = avatarPath
else: else:
profilePic = posts[0].userProfilePic profilePic = posts[0].userProfilePic
#print("--- {} seconds fetching feed---".format(time.time() - start_time)) print("--- {} seconds fetching twitter feed---".format(time.time() - start_time))
return render_template('index.html', title='Home', posts=posts, avatar=avatarPath, profilePic = profilePic, followedCount=followed, form=form) return render_template('index.html', title='Home', posts=posts, avatar=avatarPath, profilePic = profilePic, followedCount=followed, form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/settings')
@login_required
def settings():
return render_template('settings.html')
@app.route('/export')
@login_required
#Export data into a JSON file. Later you can import the data.
def export():
a = exportData()
if a:
return send_from_directory('.', 'data_export.json', as_attachment=True)
else:
return redirect(url_for('error/405'))
def exportData():
twitterFollowing = current_user.following_list()
youtubeFollowing = current_user.youtube_following_list()
data = {}
data['twitter'] = []
data['youtube'] = []
for f in twitterFollowing:
data['twitter'].append({
'username': f.username
})
for f in youtubeFollowing:
data['youtube'].append({
'channelId': f.channelId
})
try:
with open('app/data_export.json', 'w') as outfile:
json.dump(data, outfile)
return True
except:
return False
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
if isTwitterUser(form.username.data):
flash('This is username is taken! Choose a different one.')
else:
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
@app.route('/invidious', methods=['GET', 'POST'])
@login_required
def invidious():
form = ChannelForm()
if form.validate_on_submit():
channelId = form.channelId.data
if requests.get('https://invidio.us/feed/channel/{}'.format(channelId)).status_code == 200:
follow = invidiousFollow()
follow.channelId = channelId
follow.followers.append(current_user)
try:
db.session.add(follow)
db.session.commit()
flash("Added to list!")
except:
flash("Something went wrong. Try again!")
return redirect(url_for('invidious'))
else:
flash("Enter a valid Channel ID. Eg: UCJWCJCWOxBYSi5DhCieLOLQ")
return redirect(url_for('invidious'))
ids = current_user.youtube_following_list()
videos = getInvidiousPosts(ids)
if videos:
videos.sort(key=lambda x: x.date, reverse=True)
return render_template('invidious.html', videos=videos, form=form)
@app.route('/ytsearch', methods=['GET', 'POST'])
@login_required
def ytsearch():
form = ChannelForm()
button_form = EmptyForm()
if form.validate_on_submit():
channelId = form.channelId.data
r = requests.get('https://invidio.us/api/v1/search?type=channel&q={}'.format(channelId))
if r.status_code == 200:
results = json.loads(r.content)
channels = []
for res in results:
channels.append({
'username':res['author'],
'channelId':res['authorId'],
'thumbnail':res['authorThumbnails'][0]['url'],
'subCount':letterify(res['subCount'])
})
return render_template('ytsearch.html', form=form, btform=button_form, results=channels)
else:
return render_template('ytsearch.html', form=form)
@app.route('/savePost/<url>', methods=['POST']) @app.route('/savePost/<url>', methods=['POST'])
@login_required @login_required
def savePost(url): def savePost(url):
@ -204,43 +76,6 @@ def deleteSaved(id):
db.session.commit() db.session.commit()
return redirect(url_for('saved')) return redirect(url_for('saved'))
@app.route('/ytfollow/<channelId>', methods=['POST'])
@login_required
def ytfollow(channelId):
form = EmptyForm()
if form.validate_on_submit():
channel = invidiousFollow.query.filter_by(channelId=channelId).first()
if requests.get('https://invidio.us/feed/channel/{}'.format(channelId)).status_code == 200:
if channel is None:
follow = invidiousFollow()
follow.channelId = channelId
follow.followers.append(current_user)
try:
db.session.add(follow)
db.session.commit()
except:
flash("Something went wrong. Try again!")
return redirect(url_for('invidious'))
flash('You are following {}!'.format(channelId))
else:
flash("Something went wrong... try again")
return redirect(url_for('ytsearch'))
else:
return redirect(url_for('ytsearch'))
@app.route('/ytunfollow/<channelId>', methods=['POST'])
@login_required
def ytunfollow(channelId):
form = EmptyForm()
channel = invidiousFollow.query.filter_by(channelId=channelId).first()
try:
db.session.delete(channel)
db.session.commit()
flash("User unfollowed!")
except:
flash("There was an error unfollowing the user. Try again.")
return redirect(url_for('ytsearch'))
@app.route('/follow/<username>', methods=['POST']) @app.route('/follow/<username>', methods=['POST'])
@login_required @login_required
def follow(username): def follow(username):
@ -331,12 +166,6 @@ def search():
else: else:
return render_template('search.html', form = form) return render_template('search.html', form = form)
@app.route('/error/<errno>')
def error(errno):
return render_template('{}.html'.format(str(errno)))
@app.route('/user/<username>') @app.route('/user/<username>')
@login_required @login_required
def user(username): def user(username):
@ -362,6 +191,219 @@ def user(username):
profilePic = posts[0].userProfilePic profilePic = posts[0].userProfilePic
return render_template('user.html', user=user, posts=posts, profilePic = profilePic, form=form) return render_template('user.html', user=user, posts=posts, profilePic = profilePic, form=form)
#########################
#### Youtube Logic ######
#########################
@app.route('/invidious', methods=['GET', 'POST'])
@login_required
def invidious():
start_time = time.time()
form = ChannelForm()
if form.validate_on_submit():
channelId = form.channelId.data
if requests.get('https://{instance}/feed/channel/{cid}'.format(instance=invidiousInstance, cid=channelId)).status_code == 200:
follow = invidiousFollow()
follow.channelId = channelId
follow.followers.append(current_user)
try:
db.session.add(follow)
db.session.commit()
flash("Added to list!")
except:
flash("Something went wrong. Try again!")
return redirect(url_for('invidious'))
else:
flash("Enter a valid Channel ID. Eg: UCJWCJCWOxBYSi5DhCieLOLQ")
return redirect(url_for('invidious'))
ids = current_user.youtube_following_list()
videos = getInvidiousPosts(ids)
if videos:
videos.sort(key=lambda x: x.date, reverse=True)
print("--- {} seconds fetching invidious feed---".format(time.time() - start_time))
return render_template('invidious.html', videos=videos, form=form)
@app.route('/ytsearch', methods=['GET', 'POST'])
@login_required
def ytsearch():
form = ChannelForm()
button_form = EmptyForm()
if form.validate_on_submit():
channelId = form.channelId.data
c = requests.get('https://{instance}/api/v1/search?type=channel&q={cid}'.format(instance=invidiousInstance, cid=channelId))
v = requests.get('https://{instance}/api/v1/search?type=video&q={cid}'.format(instance=invidiousInstance, cid=channelId))
if c.status_code == 200 and v.status_code == 200:
results = json.loads(c.content)
channels = []
videos = []
for res in results:
channels.append({
'username':res['author'],
'channelId':res['authorId'],
'thumbnail':res['authorThumbnails'][0]['url'],
'subCount':letterify(res['subCount'])
})
results = json.loads(v.content)
for data in results:
videos.append({
'instance':invidiousInstance,
'author':data['author'],
'videoTitle':data['title'],
'description':Markup(data['description'][0:125]+'...'),
'id':data['videoId'],
'videoThumb': data['videoThumbnails'][4]['url'],
'channelUrl':data['authorUrl'],
'views':data['viewCount'],
'timeStamp':data['publishedText']
})
return render_template('ytsearch.html', form=form, btform=button_form, results=channels, videos=videos)
else:
return render_template('ytsearch.html', form=form)
@app.route('/ytfollow/<channelId>', methods=['POST'])
@login_required
def ytfollow(channelId):
form = EmptyForm()
if form.validate_on_submit():
channel = invidiousFollow.query.filter_by(channelId=channelId).first()
if requests.get('https://{instance}/feed/channel/{cid}'.format(instance=invidiousInstance, cid=channelId)).status_code == 200:
if channel is None:
follow = invidiousFollow()
follow.channelId = channelId
follow.followers.append(current_user)
try:
db.session.add(follow)
db.session.commit()
except:
flash("Something went wrong. Try again!")
return redirect(url_for('invidious'))
flash('You are following {}!'.format(channelId))
else:
flash("Something went wrong... try again")
return redirect(url_for('ytsearch'))
else:
return redirect(url_for('ytsearch'))
@app.route('/ytunfollow/<channelId>', methods=['POST'])
@login_required
def ytunfollow(channelId):
form = EmptyForm()
channel = invidiousFollow.query.filter_by(channelId=channelId).first()
try:
db.session.delete(channel)
db.session.commit()
flash("User unfollowed!")
except:
flash("There was an error unfollowing the user. Try again.")
return redirect(url_for('ytsearch'))
@app.route('/video/<id>', methods=['POST', 'GET'])
@login_required
def video(id):
data = requests.get('https://{instance}/api/v1/videos/{id}'.format(instance=invidiousInstance, id=id))
data = json.loads(data.content)
video = {
'title':data['title'],
'description':Markup(data['descriptionHtml']),
'viewCount':data['viewCount'],
'likeCount':data['likeCount'],
'dislikeCount':data['dislikeCount'],
'authorThumb':data['authorThumbnails'][4]['url'],
'author':data['author'],
'authorUrl':data['authorUrl'],
'instance':invidiousInstance,
'id':id
}
return render_template("video.html", video=video)
#########################
#### General Logic ######
#########################
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/settings')
@login_required
def settings():
return render_template('settings.html')
@app.route('/export')
@login_required
#Export data into a JSON file. Later you can import the data.
def export():
a = exportData()
if a:
return send_from_directory('.', 'data_export.json', as_attachment=True)
else:
return redirect(url_for('error/405'))
def exportData():
twitterFollowing = current_user.following_list()
youtubeFollowing = current_user.youtube_following_list()
data = {}
data['twitter'] = []
data['youtube'] = []
for f in twitterFollowing:
data['twitter'].append({
'username': f.username
})
for f in youtubeFollowing:
data['youtube'].append({
'channelId': f.channelId
})
try:
with open('app/data_export.json', 'w') as outfile:
json.dump(data, outfile)
return True
except:
return False
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
if isTwitterUser(form.username.data):
flash('This is username is taken! Choose a different one.')
else:
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
@app.route('/error/<errno>')
def error(errno):
return render_template('{}.html'.format(str(errno)))
def getTimeDiff(t): def getTimeDiff(t):
tweetTime = datetime.datetime(*t[:6]) tweetTime = datetime.datetime(*t[:6])
diff = datetime.datetime.now() - tweetTime diff = datetime.datetime.now() - tweetTime
@ -459,7 +501,7 @@ def getPosts(account):
def getInvidiousPosts(ids): def getInvidiousPosts(ids):
videos = [] videos = []
with FuturesSession() as session: with FuturesSession() as session:
futures = [session.get('https://invidio.us/feed/channel/{}'.format(id.channelId)) for id in ids] futures = [session.get('https://{instance}/feed/channel/{id}'.format(instance=invidiousInstance, id=id.channelId)) for id in ids]
for future in as_completed(futures): for future in as_completed(futures):
resp = future.result() resp = future.result()
rssFeed=feedparser.parse(resp.content) rssFeed=feedparser.parse(resp.content)
@ -470,6 +512,7 @@ def getInvidiousPosts(ids):
video.channelName = vid.author_detail.name video.channelName = vid.author_detail.name
video.channelUrl = vid.author_detail.href video.channelUrl = vid.author_detail.href
video.videoUrl = vid.link video.videoUrl = vid.link
video.id = vid.link.split("?v=")[1]
video.videoTitle = vid.title video.videoTitle = vid.title
video.videoThumb = vid.media_thumbnail[0]['url'] video.videoThumb = vid.media_thumbnail[0]['url']
video.views = vid.media_statistics['views'] video.views = vid.media_statistics['views']

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
app/static/semantic/fonts/OFL.txt Executable file → Normal file
View File

@ -1,5 +1,4 @@
Copyright (c) 2010-2015, Łukasz Dziedzic (dziedzic@typoland.com), Copyright 2011 The Mulish Project Authors (github.com/googlefonts/mulish)
with Reserved Font Name Lato.
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: This license is copied below, and is also available with a FAQ at:

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
<img src="{{video.videoThumb}}"> <img src="{{video.videoThumb}}">
</div> </div>
<div class="content"> <div class="content">
<a class="video-title" target="_blank" href="{{video.videoUrl}}">{{video.videoTitle}}</a> <a class="video-title" href="{{url_for('video', id=video.id)}}">{{video.videoTitle}}</a>
<div class="meta"> <div class="meta">
<a href="{{video.channelUrl}}">{{video.channelName}}</a> <a href="{{video.channelUrl}}">{{video.channelName}}</a>
</div> </div>

View File

@ -1,27 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<br> <br>
<div class="ui one column centered grid"> <br>
<form class="ui form" action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.channelId.label }}<br>
{{ form.channelId(size=32) }}<br>
{% for error in form.channelId.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
</div>
<br>
<br>
{% if videos %} {% if videos %}
<div class="ui centered cards"> <div class="ui centered cards">
{% for video in videos %} {% for video in videos %}
{% include '_video.html' %} {% include '_video_item.html' %}
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}

43
app/templates/video.html Normal file
View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div style="margin-top: 2em;" class="ui one column centered grid">
<iframe id='ivplayer' width='640' height='360' src='https://{{video.instance}}/embed/{{video.id}}' style='border:none;'>
</iframe>
</div>
<div style="margin-top: 2em;" class="ui one column center aligned grid">
<a href="https://{{video.instance}}/watch?v={{video.id}}"><h2 class="ui header">{{video.title}}</h2></a>
</div>
<div style="margin-top: 2em;" class="ui one column center aligned grid">
<a target="_blank" href="https://{{video.instance}}{{video.authorUrl}}" class="ui image label">
<img src="{{video.authorThumb}}">
{%if video.author.__len__() > 8%}
{{video.author[0:8]+'...'}}
{%else%}
{{video.author}}
{%endif%}
</a>
<div class="ui label">
<i class="eye icon"></i> {{video.viewCount}}
</div>
<div class="ui label">
<i class="thumbs up green icon"></i> {{video.likeCount}}
</div>
<div class="ui label">
<i class="thumbs down red icon"></i> {{video.dislikeCount}}
</div>
</div>
<div style="margin-top: 2em;" class="ui one column center aligned grid">
<div style="margin-bottom: 2em;" class="ui comments">
<h3 class="ui dividing header">Description</h3>
{{video.description}}
</div>
</div>
{% endblock %}

View File

@ -14,9 +14,19 @@
<p>{{ form.submit() }}</p> <p>{{ form.submit() }}</p>
</form> </form>
<div class="ui one column middle aligned grid">
<div class="ui message">
<div class="header">
Tip: Videos are shown below channels.
</div>
<p>Just scroll down!</p>
</div>
</div>
{% if results %} {% if results %}
<div class="ui one column centered grid"> <div class="ui one column centered grid">
<div class="ui middle aligned divided list"> <div class="ui middle aligned divided list">
<h3 class="ui dividing header">Users</h3>
{% for res in results %} {% for res in results %}
<div class="item"> <div class="item">
<div class="right floated content"> <div class="right floated content">
@ -46,6 +56,20 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="ui middle aligned divided list">
<h3 class="ui dividing header">Videos</h3>
{% if videos %}
<div class="ui centered cards">
{% for video in videos %}
{% include '_video_item.html' %}
{% endfor %}
</div>
{% else %}
{% include '_empty_feed.html' %}
{% endif %}
</div>
</div> </div>
{% endif %} {% endif %}
</div> </div>