This commit is contained in:
parent
d02ae5bd3f
commit
be03e7b931
5
docs/features.md
Normal file
5
docs/features.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
## Просмотры публикаций
|
||||||
|
|
||||||
|
- Интеграция с Google Analytics для отслеживания просмотров публикаций
|
||||||
|
- Подсчет уникальных пользователей и общего количества просмотров
|
||||||
|
- Автоматическое обновление статистики при запросе данных публикации
|
|
@ -7,12 +7,8 @@ from typing import Dict
|
||||||
|
|
||||||
# ga
|
# ga
|
||||||
from google.analytics.data_v1beta import BetaAnalyticsDataClient
|
from google.analytics.data_v1beta import BetaAnalyticsDataClient
|
||||||
from google.analytics.data_v1beta.types import (
|
from google.analytics.data_v1beta.types import DateRange, Dimension, Metric, RunReportRequest
|
||||||
DateRange,
|
from google.analytics.data_v1beta.types import Filter as GAFilter
|
||||||
Dimension,
|
|
||||||
Metric,
|
|
||||||
RunReportRequest,
|
|
||||||
)
|
|
||||||
|
|
||||||
from orm.author import Author
|
from orm.author import Author
|
||||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||||
|
@ -27,8 +23,8 @@ VIEWS_FILEPATH = "/dump/views.json"
|
||||||
|
|
||||||
class ViewedStorage:
|
class ViewedStorage:
|
||||||
lock = asyncio.Lock()
|
lock = asyncio.Lock()
|
||||||
views_by_shout_slug = {}
|
precounted_by_slug = {}
|
||||||
views_by_shout_id = {}
|
views_by_shout = {}
|
||||||
shouts_by_topic = {}
|
shouts_by_topic = {}
|
||||||
shouts_by_author = {}
|
shouts_by_author = {}
|
||||||
views = None
|
views = None
|
||||||
|
@ -84,23 +80,16 @@ class ViewedStorage:
|
||||||
|
|
||||||
with open(viewfile_path, "r") as file:
|
with open(viewfile_path, "r") as file:
|
||||||
precounted_views = json.load(file)
|
precounted_views = json.load(file)
|
||||||
self.views_by_shout_slug.update(precounted_views)
|
self.precounted_by_slug.update(precounted_views)
|
||||||
logger.info(f" * {len(precounted_views)} shouts with views was loaded.")
|
logger.info(f" * {len(precounted_views)} shouts with views was loaded.")
|
||||||
|
|
||||||
# get shout_id by slug
|
|
||||||
with local_session() as session:
|
|
||||||
for slug, views_count in self.views_by_shout_slug.items():
|
|
||||||
shout_id = session.query(Shout.id).filter(Shout.slug == slug).scalar()
|
|
||||||
if isinstance(shout_id, int):
|
|
||||||
self.views_by_shout_id.update({shout_id: views_count})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"precounted views loading error: {e}")
|
logger.error(f"precounted views loading error: {e}")
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_pages():
|
async def update_pages():
|
||||||
"""Запрос всех страниц от Google Analytics, отсортированных по количеству просмотров"""
|
"""Запрос всех страниц от Google Analytics, отсортрованных по количеству просмотров"""
|
||||||
self = ViewedStorage
|
self = ViewedStorage
|
||||||
logger.info(" ⎧ views update from Google Analytics ---")
|
logger.info(" ⎧ views update from Google Analytics ---")
|
||||||
if self.running:
|
if self.running:
|
||||||
|
@ -126,11 +115,11 @@ class ViewedStorage:
|
||||||
if isinstance(row.dimension_values, list):
|
if isinstance(row.dimension_values, list):
|
||||||
page_path = row.dimension_values[0].value
|
page_path = row.dimension_values[0].value
|
||||||
slug = page_path.split("discours.io/")[-1]
|
slug = page_path.split("discours.io/")[-1]
|
||||||
views_count = int(row.metric_values[0].value)
|
fresh_views = int(row.metric_values[0].value)
|
||||||
|
|
||||||
# Обновление данных в хранилище
|
# Обновление данных в хранилище
|
||||||
self.views_by_shout[slug] = self.views_by_shout.get(slug, 0)
|
self.views_by_shout[slug] = self.views_by_shout.get(slug, 0)
|
||||||
self.views_by_shout[slug] += views_count
|
self.views_by_shout[slug] += fresh_views
|
||||||
self.update_topics(slug)
|
self.update_topics(slug)
|
||||||
|
|
||||||
# Запись путей страниц для логирования
|
# Запись путей страниц для логирования
|
||||||
|
@ -148,12 +137,17 @@ class ViewedStorage:
|
||||||
def get_shout(shout_slug="", shout_id=0) -> int:
|
def get_shout(shout_slug="", shout_id=0) -> int:
|
||||||
"""Получение метрики просмотров shout по slug или id."""
|
"""Получение метрики просмотров shout по slug или id."""
|
||||||
self = ViewedStorage
|
self = ViewedStorage
|
||||||
return self.views_by_shout_slug.get(shout_slug, self.views_by_shout_id.get(shout_id, 0))
|
fresh_views = self.views_by_shout.get(shout_slug, 0)
|
||||||
|
precounted_views = self.precounted_by_slug.get(shout_slug, 0)
|
||||||
|
return fresh_views + precounted_views
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_shout_media(shout_slug) -> Dict[str, int]:
|
def get_shout_media(shout_slug) -> Dict[str, int]:
|
||||||
"""Получение метрики воспроизведения shout по slug."""
|
"""Получение метрики воспроизведения shout по slug."""
|
||||||
self = ViewedStorage
|
self = ViewedStorage
|
||||||
|
|
||||||
|
# TODO: get media plays from Google Analytics
|
||||||
|
|
||||||
return self.views_by_shout.get(shout_slug, 0)
|
return self.views_by_shout.get(shout_slug, 0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -173,7 +167,7 @@ class ViewedStorage:
|
||||||
"""Обновление счетчиков темы по slug shout"""
|
"""Обновление счетчиков темы по slug shout"""
|
||||||
self = ViewedStorage
|
self = ViewedStorage
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
# Определение вспомогательной функции для избежания повторения кода
|
# Определение вспомогательной функции для избежа<EFBFBD><EFBFBD>ия повторения кода
|
||||||
def update_groups(dictionary, key, value):
|
def update_groups(dictionary, key, value):
|
||||||
dictionary[key] = list(set(dictionary.get(key, []) + [value]))
|
dictionary[key] = list(set(dictionary.get(key, []) + [value]))
|
||||||
|
|
||||||
|
@ -222,3 +216,50 @@ class ViewedStorage:
|
||||||
else:
|
else:
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
logger.info(" - try to update views again")
|
logger.info(" - try to update views again")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_slug_views(slug: str) -> int:
|
||||||
|
"""
|
||||||
|
Получает fresh статистику просмотров для указанного slug.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug: Идентификатор страницы
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Количество просмотров
|
||||||
|
"""
|
||||||
|
self = ViewedStorage
|
||||||
|
if not self.analytics_client:
|
||||||
|
logger.warning("Google Analytics client not initialized")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем фильтр для точного совпадения конца URL
|
||||||
|
request = RunReportRequest(
|
||||||
|
property=f"properties/{GOOGLE_PROPERTY_ID}",
|
||||||
|
date_ranges=[DateRange(start_date=self.start_date, end_date="today")],
|
||||||
|
dimensions=[Dimension(name="pagePath")],
|
||||||
|
dimension_filter=GAFilter(
|
||||||
|
field_name="pagePath",
|
||||||
|
string_filter=GAFilter.StringFilter(
|
||||||
|
value=f".*/{slug}$", # Используем регулярное выражение для точного совпадения конца URL
|
||||||
|
match_type=GAFilter.StringFilter.MatchType.FULL_REGEXP,
|
||||||
|
case_sensitive=False, # Включаем чувствительность к регистру для точности
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metrics=[Metric(name="screenPageViews")],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.analytics_client.run_report(request)
|
||||||
|
|
||||||
|
if not response.rows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
views = int(response.rows[0].metric_values[0].value)
|
||||||
|
# Кэшируем результат
|
||||||
|
self.views_by_shout[slug] = views
|
||||||
|
return views
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Google Analytics API Error: {e}")
|
||||||
|
return 0
|
||||||
|
|
Loading…
Reference in New Issue
Block a user