mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Added rating and views to video information.
This commit is contained in:
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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 */
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
@ -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>•</span>
|
||||
<span>{{ video.publish_date | naturaltime }}</span>
|
||||
</p>
|
||||
<p class="card-text">{{ video.description | truncatechars:120 }}</p>
|
||||
</div>
|
||||
<div class="card-footer dropdown show">
|
||||
|
0
YtManagerApp/templatetags/__init__.py
Normal file
0
YtManagerApp/templatetags/__init__.py
Normal file
30
YtManagerApp/templatetags/common.py
Normal file
30
YtManagerApp/templatetags/common.py
Normal 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])
|
61
YtManagerApp/templatetags/ratings.py
Normal file
61
YtManagerApp/templatetags/ratings.py
Normal 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])
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
Reference in New Issue
Block a user