Added rating and views to video information.

This commit is contained in:
2018-10-22 01:02:51 +03:00
parent 6fd1c0a963
commit ab47484f54
13 changed files with 413 additions and 287 deletions

View File

@ -21,10 +21,15 @@ def __check_new_videos_sub(subscription: Subscription, yt_api: YoutubeAPI):
results = Video.objects.filter(video_id=video.getVideoId(), subscription=subscription)
if len(results) == 0:
log.info('New video for subscription %s: %s %s"', subscription, video.getVideoId(), video.getTitle())
create_video(video, subscription)
db_video = create_video(video, subscription)
else:
# TODO... update view count, rating etc
pass
db_video = results.first()
# Update video stats - rating and view count
stats = yt_api.get_single_video_stats(db_video.video_id)
db_video.rating = stats.get_like_count() / (stats.get_like_count() + stats.get_dislike_count())
db_video.views = stats.get_view_count()
db_video.save()
def __detect_deleted(subscription: Subscription):

View File

@ -19,6 +19,7 @@ def create_video(yt_video: YoutubePlaylistItem, subscription: Subscription):
video.icon_default = yt_video.getDefaultThumbnailUrl()
video.icon_best = yt_video.getBestThumbnailUrl()
video.save()
return video
def get_videos(user: User,

View File

@ -92,6 +92,8 @@
margin-right: -0.25rem; }
.video-gallery .card .card-more:hover {
text-decoration: none; }
.video-gallery .card .progress {
width: 100px; }
.video-gallery .video-icon-yes {
color: #007bff; }
.video-gallery .video-icon-no {
@ -110,4 +112,8 @@
.modal-field-error ul {
margin: 0; }
.star-rating {
display: inline-block;
margin-bottom: 0.5rem; }
/*# sourceMappingURL=style.css.map */

View File

@ -1,6 +1,6 @@
{
"version": 3,
"mappings": "AAEA,OAAQ;EACJ,QAAQ,EAAE,KAAK;EACf,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,SAAS;EAClB,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,MAAM;EACrB,SAAS,EAAE,IAAI;;AAqBnB,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AASjC,8BAAgB;EACZ,KAAK,EA/GE,OAAO;AAiHlB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC",
"mappings": "AAEA,OAAQ;EACJ,QAAQ,EAAE,KAAK;EACf,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,SAAS;EAClB,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,MAAM;EACrB,SAAS,EAAE,IAAI;;AAqBnB,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAO7B,8BAAU;EACN,KAAK,EAAE,KAAK;AAKpB,8BAAgB;EACZ,KAAK,EAnHE,OAAO;AAqHlB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC;;AAIjB,YAAa;EACT,OAAO,EAAE,YAAY;EACrB,aAAa,EAAE,MAAM",
"sources": ["style.scss"],
"names": [],
"file": "style.css"

View File

@ -104,8 +104,12 @@ $accent-color: #007bff;
}
.card-img-top {
}
.progress {
width: 100px;
}
}
.video-icon-yes {
@ -134,5 +138,9 @@ $accent-color: #007bff;
ul {
margin: 0;
}
}
.star-rating {
display: inline-block;
margin-bottom: 0.5rem;
}

View File

@ -1,3 +1,6 @@
{% load humanize %}
{% load ratings %}
<div class="video-gallery container-fluid">
<div class="row">
{% for video in videos %}
@ -11,6 +14,11 @@
{% endif %}
{{ video.name }}
</h5>
<p class="card-text small text-muted">
<span>{{ video.views | intcomma }} views</span>
<span>&#x2022;</span>
<span>{{ video.publish_date | naturaltime }}</span>
</p>
<p class="card-text">{{ video.description | truncatechars:120 }}</p>
</div>
<div class="card-footer dropdown show">

View File

View File

@ -0,0 +1,30 @@
from django import template
register = template.Library()
class SetVarNode(template.Node):
def __init__(self, var_name, var_value):
self.var_name = var_name
self.var_value = var_value
def render(self, context):
try:
value = template.Variable(self.var_value).resolve(context)
except template.VariableDoesNotExist:
value = ""
context[self.var_name] = value
return u""
@register.tag(name='set')
def set_var(parser, token):
"""
{% set some_var = '123' %}
"""
parts = token.split_contents()
if len(parts) < 4:
raise template.TemplateSyntaxError("'set' tag must be of the form: {% set <var_name> = <var_value> %}")
return SetVarNode(parts[1], parts[3])

View File

@ -0,0 +1,61 @@
from django import template
register = template.Library()
FULL_STAR_CLASS = "typcn-star-full-outline"
HALF_STAR_CLASS = "typcn-star-half-outline"
EMPTY_STAR_CLASS = "typcn-star-outline"
class StarRatingNode(template.Node):
def __init__(self, rating_percent, max_stars="5"):
self.rating = rating_percent
self.max_stars = max_stars
def render(self, context):
try:
rating = template.Variable(self.rating).resolve(context)
except template.VariableDoesNotExist:
rating = 0
try:
max_stars = template.Variable(self.max_stars).resolve(context)
except template.VariableDoesNotExist:
max_stars = 0
total_halves = (max_stars - 1) * rating * 2
html = [
f'<div class="star-rating" title="{ 1 + (total_halves / 2):.2f} stars">'
f'<span class="typcn {FULL_STAR_CLASS}"></span>'
]
for i in range(max_stars - 1):
if total_halves >= 2 * i + 2:
cls = FULL_STAR_CLASS
elif total_halves >= 2 * i + 1:
cls = HALF_STAR_CLASS
else:
cls = EMPTY_STAR_CLASS
html.append(f'<span class="typcn {cls}"></span>')
html.append("</div>")
return u"".join(html)
@register.tag(name='starrating')
def star_rating_tag(parser, token):
"""
{% rating percent [max_stars=5]%}
"""
parts = token.split_contents()
if len(parts) <= 1:
raise template.TemplateSyntaxError("'set' tag must be of the form: {% rating <value_percent> [<max_stars>=5] %}")
if len(parts) <= 2:
return StarRatingNode(parts[1])
return StarRatingNode(parts[1], parts[2])

View File

@ -1,3 +1,7 @@
import itertools
from typing import Iterable
def first_true(*args, default=False, pred=None):
"""Returns the first true value in the iterable.
@ -10,3 +14,19 @@ def first_true(*args, default=False, pred=None):
# first_true([a,b,c], x) --> a or b or c or x
# first_true([a,b], x, f) --> a if f(a) else b if f(b) else x
return next(filter(pred, args), default)
def as_chunks(iterable: Iterable, chunk_size: int):
"""
Iterates an iterable in chunks of chunk_size elements.
:param iterable: An iterable containing items to iterate.
:param chunk_size: Chunk size
:return: Returns a generator which will yield chunks of size chunk_size
"""
it = iter(iterable)
while True:
chunk = tuple(itertools.islice(it, chunk_size))
if not chunk:
return
yield chunk

View File

@ -3,10 +3,13 @@ from googleapiclient.errors import Error as APIError
from google_auth_oauthlib.flow import InstalledAppFlow
from django.conf import settings
import re
from YtManagerApp.utils.iterutils import as_chunks
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
YOUTUBE_LIST_LIMIT = 50
class YoutubeException(Exception):
pass
@ -28,6 +31,10 @@ class YoutubePlaylistNotFoundException(YoutubeException):
pass
class YoutubeVideoNotFoundException(YoutubeException):
pass
class YoutubeChannelInfo(object):
def __init__(self, result_dict):
self.__id = result_dict['id']
@ -130,6 +137,27 @@ class YoutubePlaylistItem(object):
return self.__snippet['position']
class YoutubeVideoStatistics(object):
def __init__(self, result_dict):
self.id = result_dict['id']
self.stats = result_dict['statistics']
def get_view_count(self):
return int(self.stats['viewCount'])
def get_like_count(self):
return int(self.stats['likeCount'])
def get_dislike_count(self):
return int(self.stats['dislikeCount'])
def get_favorite_count(self):
return int(self.stats['favoriteCount'])
def get_comment_count(self):
return int(self.stats['commentCount'])
class YoutubeAPI(object):
def __init__(self, service):
self.service = service
@ -230,6 +258,26 @@ class YoutubeAPI(object):
else:
last_page = True
def get_single_video_stats(self, video_id) -> YoutubeVideoStatistics:
result = list(self.get_video_stats([video_id]))
if len(result) < 1:
raise YoutubeVideoNotFoundException('Could not find video with id ' + video_id + '!')
return result[0]
def get_video_stats(self, video_id_list):
for chunk in as_chunks(video_id_list, YOUTUBE_LIST_LIMIT):
kwargs = {
"part": "statistics",
"maxResults": YOUTUBE_LIST_LIMIT,
"id": ','.join(chunk)
}
result = self.service.videos()\
.list(**kwargs)\
.execute()
for item in result['items']:
yield YoutubeVideoStatistics(item)
# @staticmethod
# def build_oauth() -> 'YoutubeAPI':
# flow =