methods.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. # Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
  2. # related content
  3. import logging
  4. import random
  5. import itertools
  6. from datetime import datetime
  7. from cms import celery_app
  8. from django.conf import settings
  9. from django.core.cache import cache
  10. from django.db.models import Q
  11. from django.core.mail import EmailMessage
  12. from . import models
  13. from .helpers import mask_ip
  14. logger = logging.getLogger(__name__)
  15. def get_user_or_session(request):
  16. """Return a dictionary with user info
  17. whether user is authenticated or not
  18. this is used in action calculations, example for
  19. increasing the watch counter of a media
  20. """
  21. ret = {}
  22. if request.user.is_authenticated:
  23. ret["user_id"] = request.user.id
  24. else:
  25. if not request.session.session_key:
  26. request.session.save()
  27. ret["user_session"] = request.session.session_key
  28. if settings.MASK_IPS_FOR_ACTIONS:
  29. ret["remote_ip_addr"] = mask_ip(request.META.get("REMOTE_ADDR"))
  30. else:
  31. ret["remote_ip_addr"] = request.META.get("REMOTE_ADDR")
  32. return ret
  33. def pre_save_action(media, user, session_key, action, remote_ip):
  34. """This will perform some checkes
  35. example threshold checks, before performing an action
  36. """
  37. from actions.models import MediaAction
  38. if user:
  39. query = MediaAction.objects.filter(media=media, action=action, user=user)
  40. else:
  41. query = MediaAction.objects.filter(
  42. media=media, action=action, session_key=session_key
  43. )
  44. query = query.order_by("-action_date")
  45. if query:
  46. query = query.first()
  47. if action in ["like", "dislike", "report"]:
  48. return False # has alread done action once
  49. elif action == "watch" and user:
  50. # increase the number of times a media is viewed
  51. if media.duration:
  52. now = datetime.now(query.action_date.tzinfo)
  53. if (now - query.action_date).seconds > media.duration:
  54. return True
  55. else:
  56. if user: # first time action
  57. return True
  58. if not user:
  59. # perform some checking for requests where no session
  60. # id is specified (and user is anonymous) to avoid spam
  61. # eg allow for the same remote_ip for a specific number of actions
  62. query = (
  63. MediaAction.objects.filter(media=media, action=action, remote_ip=remote_ip)
  64. .filter(user=None)
  65. .order_by("-action_date")
  66. )
  67. if query:
  68. query = query.first()
  69. now = datetime.now(query.action_date.tzinfo)
  70. if action == "watch":
  71. if not (now - query.action_date).seconds > media.duration:
  72. return False
  73. if (now - query.action_date).seconds > settings.TIME_TO_ACTION_ANONYMOUS:
  74. return True
  75. else:
  76. return True
  77. return False
  78. def is_mediacms_editor(user):
  79. """Whether user is MediaCMS editor"""
  80. editor = False
  81. try:
  82. if user.is_superuser or user.is_manager or user.is_editor:
  83. editor = True
  84. except BaseException:
  85. pass
  86. return editor
  87. def is_mediacms_manager(user):
  88. """Whether user is MediaCMS manager"""
  89. manager = False
  90. try:
  91. if user.is_superuser or user.is_manager:
  92. manager = True
  93. except BaseException:
  94. pass
  95. return manager
  96. def get_next_state(user, current_state, next_state):
  97. """Return valid state, given a current and next state
  98. and the user object.
  99. Users may themselves perform only allowed transitions
  100. """
  101. if next_state not in ["public", "private", "unlisted"]:
  102. next_state = settings.PORTAL_WORKFLOW # get default state
  103. if is_mediacms_editor(user):
  104. # allow any transition
  105. return next_state
  106. if settings.PORTAL_WORKFLOW == "private":
  107. next_state = "private"
  108. if settings.PORTAL_WORKFLOW == "unlisted":
  109. # don't allow to make media public in this case
  110. if next_state == "public":
  111. next_state = current_state
  112. return next_state
  113. def notify_users(friendly_token=None, action=None, extra=None):
  114. """Notify users through email, for a set of actions"""
  115. notify_items = []
  116. media = None
  117. if friendly_token:
  118. media = models.Media.objects.filter(friendly_token=friendly_token).first()
  119. if not media:
  120. return False
  121. media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
  122. if action == "media_reported" and media:
  123. msg = """
  124. Media %s was reported.
  125. Reason: %s\n
  126. Total times this media has been reported: %s\n
  127. Media becomes private if it gets reported %s times %s\n
  128. """ % (
  129. media_url,
  130. extra,
  131. media.reported_times,
  132. settings.REPORTED_TIMES_THRESHOLD,
  133. )
  134. if settings.ADMINS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
  135. title = "[{}] - Media was reported".format(settings.PORTAL_NAME)
  136. d = {}
  137. d["title"] = title
  138. d["msg"] = msg
  139. d["to"] = settings.ADMIN_EMAIL_LIST
  140. notify_items.append(d)
  141. if settings.USERS_NOTIFICATIONS.get("MEDIA_REPORTED", False):
  142. title = "[{}] - Media was reported".format(settings.PORTAL_NAME)
  143. d = {}
  144. d["title"] = title
  145. d["msg"] = msg
  146. d["to"] = [media.user.email]
  147. notify_items.append(d)
  148. if action == "media_added" and media:
  149. if settings.ADMINS_NOTIFICATIONS.get("MEDIA_ADDED", False):
  150. title = "[{}] - Media was added".format(settings.PORTAL_NAME)
  151. msg = """
  152. Media %s was added by user %s.
  153. """ % (
  154. media_url,
  155. media.user,
  156. )
  157. d = {}
  158. d["title"] = title
  159. d["msg"] = msg
  160. d["to"] = settings.ADMIN_EMAIL_LIST
  161. notify_items.append(d)
  162. if settings.USERS_NOTIFICATIONS.get("MEDIA_ADDED", False):
  163. title = "[{}] - Your media was added".format(settings.PORTAL_NAME)
  164. msg = """
  165. Your media has been added! It will be encoded and will be available soon.
  166. URL: %s
  167. """ % (
  168. media_url
  169. )
  170. d = {}
  171. d["title"] = title
  172. d["msg"] = msg
  173. d["to"] = [media.user.email]
  174. notify_items.append(d)
  175. if action == "comment_added" and media:
  176. if settings.USERS_NOTIFICATIONS.get("COMMENT_ADDED", False):
  177. d = {}
  178. title = f"[{settings.PORTAL_NAME}] - Comment was added"
  179. msg = f"A comment was added on media {media_url}\n"
  180. d["title"] = title
  181. d["msg"] = msg
  182. d["to"] = [media.user.username]
  183. notify_items.append(d)
  184. for item in notify_items:
  185. email = EmailMessage(
  186. item["title"], item["msg"], settings.DEFAULT_FROM_EMAIL, item["to"]
  187. )
  188. email.send(fail_silently=True)
  189. return True
  190. def show_recommended_media(request, limit=100):
  191. """Return a list of recommended media
  192. used on the index page
  193. """
  194. basic_query = Q(listable=True)
  195. pmi = cache.get("popular_media_ids")
  196. # produced by task get_list_of_popular_media and cached
  197. if pmi:
  198. media = list(
  199. models.Media.objects.filter(friendly_token__in=pmi)
  200. .filter(basic_query)
  201. .prefetch_related("user")[:limit]
  202. )
  203. else:
  204. media = list(
  205. models.Media.objects.filter(basic_query)
  206. .order_by("-views", "-likes")
  207. .prefetch_related("user")[:limit]
  208. )
  209. random.shuffle(media)
  210. return media
  211. def show_related_media(media, request=None, limit=100):
  212. """Return a list of related media"""
  213. if settings.RELATED_MEDIA_STRATEGY == "calculated":
  214. return show_related_media_calculated(media, request, limit)
  215. elif settings.RELATED_MEDIA_STRATEGY == "author":
  216. return show_related_media_author(media, request, limit)
  217. return show_related_media_content(media, request, limit)
  218. def show_related_media_content(media, request, limit):
  219. """Return a list of related media based on simple calculations"""
  220. # Create list with author items
  221. # then items on same category, then some random(latest)
  222. # Aim is to always show enough (limit) videos
  223. # and include author videos in any case
  224. q_author = Q(listable=True, user=media.user)
  225. m = list(
  226. models.Media.objects.filter(q_author)
  227. .order_by()
  228. .prefetch_related("user")[:limit]
  229. )
  230. # order by random criteria so that it doesn't bring the same results
  231. # attention: only fields that are indexed make sense here! also need
  232. # find a way for indexes with more than 1 field
  233. order_criteria = [
  234. "-views",
  235. "views",
  236. "add_date",
  237. "-add_date",
  238. "featured",
  239. "-featured",
  240. "user_featured",
  241. "-user_featured",
  242. ]
  243. # TODO: MAke this mess more readable, and add TAGS support - aka related
  244. # tags rather than random media
  245. if len(m) < limit:
  246. category = media.category.first()
  247. if category:
  248. q_category = Q(listable=True, category=category)
  249. q_res = (
  250. models.Media.objects.filter(q_category)
  251. .order_by(order_criteria[random.randint(0, len(order_criteria) - 1)])
  252. .prefetch_related("user")[: limit - media.user.media_count]
  253. )
  254. m = list(itertools.chain(m, q_res))
  255. if len(m) < limit:
  256. q_generic = Q(listable=True)
  257. q_res = (
  258. models.Media.objects.filter(q_generic)
  259. .order_by(order_criteria[random.randint(0, len(order_criteria) - 1)])
  260. .prefetch_related("user")[: limit - media.user.media_count]
  261. )
  262. m = list(itertools.chain(m, q_res))
  263. m = list(set(m[:limit])) # remove duplicates
  264. try:
  265. m.remove(media) # remove media from results
  266. except ValueError:
  267. pass
  268. random.shuffle(m)
  269. return m
  270. def show_related_media_author(media, request, limit):
  271. """Return a list of related media form the same author"""
  272. q_author = Q(listable=True, user=media.user)
  273. m = list(
  274. models.Media.objects.filter(q_author)
  275. .order_by()
  276. .prefetch_related("user")[:limit]
  277. )
  278. # order by random criteria so that it doesn't bring the same results
  279. # attention: only fields that are indexed make sense here! also need
  280. # find a way for indexes with more than 1 field
  281. m = list(set(m[:limit])) # remove duplicates
  282. try:
  283. m.remove(media) # remove media from results
  284. except ValueError:
  285. pass
  286. random.shuffle(m)
  287. return m
  288. def show_related_media_calculated(media, request, limit):
  289. """Return a list of related media based on ML recommendations
  290. A big todo!
  291. """
  292. return []
  293. def update_user_ratings(user, media, user_ratings):
  294. """Populate user ratings for a media"""
  295. for rating in user_ratings:
  296. user_rating = (
  297. models.Rating.objects.filter(
  298. user=user, media_id=media, rating_category_id=rating.get("category_id")
  299. )
  300. .only("score")
  301. .first()
  302. )
  303. if user_rating:
  304. rating["score"] = user_rating.score
  305. return user_ratings
  306. def notify_user_on_comment(friendly_token):
  307. """Notify users through email, for a set of actions"""
  308. media = None
  309. media = models.Media.objects.filter(friendly_token=friendly_token).first()
  310. if not media:
  311. return False
  312. user = media.user
  313. media_url = settings.SSL_FRONTEND_HOST + media.get_absolute_url()
  314. if user.notification_on_comments:
  315. title = "[{}] - A comment was added".format(settings.PORTAL_NAME)
  316. msg = """
  317. A comment has been added to your media %s .
  318. View it on %s
  319. """ % (
  320. media.title,
  321. media_url,
  322. )
  323. email = EmailMessage(
  324. title, msg, settings.DEFAULT_FROM_EMAIL, [media.user.email]
  325. )
  326. email.send(fail_silently=True)
  327. return True
  328. def list_tasks():
  329. """Lists celery tasks
  330. To be used in an admin dashboard
  331. """
  332. i = celery_app.control.inspect([])
  333. ret = {}
  334. temp = {}
  335. task_ids = []
  336. media_profile_pairs = []
  337. temp["active"] = i.active()
  338. temp["reserved"] = i.reserved()
  339. temp["scheduled"] = i.scheduled()
  340. for state, state_dict in temp.items():
  341. ret[state] = {}
  342. ret[state]["tasks"] = []
  343. for worker, worker_dict in state_dict.items():
  344. for task in worker_dict:
  345. task_dict = {}
  346. task_dict["worker"] = worker
  347. task_dict["task_id"] = task.get("id")
  348. task_ids.append(task.get("id"))
  349. task_dict["args"] = task.get("args")
  350. task_dict["name"] = task.get("name")
  351. task_dict["time_start"] = task.get("time_start")
  352. if task.get("name") == "encode_media":
  353. task_args = task.get("args")
  354. for bad in "(),'":
  355. task_args = task_args.replace(bad, "")
  356. friendly_token = task_args.split()[0]
  357. profile_id = task_args.split()[1]
  358. media = models.Media.objects.filter(
  359. friendly_token=friendly_token
  360. ).first()
  361. if media:
  362. profile = models.EncodeProfile.objects.filter(
  363. id=profile_id
  364. ).first()
  365. if profile:
  366. media_profile_pairs.append(
  367. (media.friendly_token, profile.id)
  368. )
  369. task_dict["info"] = {}
  370. task_dict["info"]["profile name"] = profile.name
  371. task_dict["info"]["media title"] = media.title
  372. encoding = models.Encoding.objects.filter(
  373. task_id=task.get("id")
  374. ).first()
  375. if encoding:
  376. task_dict["info"][
  377. "encoding progress"
  378. ] = encoding.progress
  379. ret[state]["tasks"].append(task_dict)
  380. ret["task_ids"] = task_ids
  381. ret["media_profile_pairs"] = media_profile_pairs
  382. return ret