|
|
import datetime
|
|
|
|
|
|
import streamlit as st
|
|
|
import io
|
|
|
import plotly.express as px
|
|
|
from wordcloud import WordCloud
|
|
|
import matplotlib.pyplot as plt
|
|
|
import plotly.graph_objects as go
|
|
|
|
|
|
from streamlit_extras.metric_cards import style_metric_cards
|
|
|
from streamlit_extras.chart_container import chart_container
|
|
|
from streamlit_extras.switch_page_button import switch_page
|
|
|
from streamlit_extras.app_logo import add_logo
|
|
|
|
|
|
from prophet import Prophet
|
|
|
|
|
|
from channelDataExtraction import getChannelData
|
|
|
from channelVideoDataExtraction import *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@st.cache_data
|
|
|
def download_data(api_key, channel_id):
|
|
|
channel_details = getChannelData(api_key, channel_id)
|
|
|
|
|
|
|
|
|
if channel_details is None:
|
|
|
return None, None, None, None
|
|
|
|
|
|
videos = getVideoList(api_key, channel_details["uploads"])
|
|
|
videos_df = pd.DataFrame(videos)
|
|
|
video_ids = [video['id'] for video in videos if video['id'] is not None]
|
|
|
all_video_data = buildVideoListDataframe(api_key, video_ids)
|
|
|
|
|
|
st.session_state.start_index = 0
|
|
|
st.session_state.end_index = 10
|
|
|
st.session_state['video_id'] = None
|
|
|
st.session_state.all_video_df = all_video_data
|
|
|
|
|
|
st.session_state.api_key = st.session_state.API_KEY
|
|
|
|
|
|
return channel_details, videos, all_video_data, videos_df
|
|
|
|
|
|
|
|
|
def display_video_list(video_data, start_index, end_index, search_query=None):
|
|
|
"""Displays a list of videos in a tabular format with custom column order and buttons."""
|
|
|
|
|
|
|
|
|
if search_query is None:
|
|
|
search_query = ""
|
|
|
new_search_query = st.text_input("Search Videos by Title", search_query)
|
|
|
|
|
|
|
|
|
if 'start_index' not in st.session_state:
|
|
|
st.session_state.start_index = start_index
|
|
|
if 'end_index' not in st.session_state:
|
|
|
st.session_state.end_index = end_index
|
|
|
|
|
|
|
|
|
if new_search_query != search_query:
|
|
|
st.session_state.start_index = start_index
|
|
|
st.session_state.end_index = end_index
|
|
|
|
|
|
|
|
|
filtered_videos = [video for video in video_data if new_search_query.lower() in video['title'].lower()]
|
|
|
|
|
|
|
|
|
paginated_videos = filtered_videos[st.session_state.start_index:st.session_state.end_index]
|
|
|
|
|
|
for video in paginated_videos:
|
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
|
with col1:
|
|
|
st.image(video['thumbnail'])
|
|
|
with col2:
|
|
|
st.write(video['id'])
|
|
|
with col3:
|
|
|
st.write(video['title'])
|
|
|
with col4:
|
|
|
video_stats = st.button("Check Video Statistics", key=video['id'])
|
|
|
if video_stats:
|
|
|
st.session_state['video_id'] = video['id']
|
|
|
switch_page("video_data")
|
|
|
|
|
|
|
|
|
if st.session_state.end_index < len(filtered_videos):
|
|
|
if st.button('Load next 10 videos', key='load_next'):
|
|
|
st.session_state.start_index = st.session_state.end_index
|
|
|
st.session_state.end_index += 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config(page_title="Youtube Channel Analytics Dashboard",
|
|
|
page_icon="📊",
|
|
|
layout="wide")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.title("YouTube Analytics Dashboard")
|
|
|
|
|
|
|
|
|
st.sidebar.title("Settings")
|
|
|
|
|
|
|
|
|
if 'API_KEY' not in st.session_state:
|
|
|
st.session_state.API_KEY = ""
|
|
|
if 'CHANNEL_ID' not in st.session_state:
|
|
|
st.session_state.CHANNEL_ID = ""
|
|
|
|
|
|
st.session_state.API_KEY = st.sidebar.text_input("Enter your YouTube API Key", st.session_state.API_KEY,
|
|
|
type="password")
|
|
|
st.session_state.CHANNEL_ID = st.sidebar.text_input("Enter the YouTube Channel ID", st.session_state.CHANNEL_ID)
|
|
|
|
|
|
if not st.session_state.API_KEY or not st.session_state.CHANNEL_ID:
|
|
|
st.warning("Please enter your API Key and Channel ID.")
|
|
|
|
|
|
user_manual_link = "https://github.com/zainmz/Youtube-Channel-Analytics-Dashboard"
|
|
|
st.markdown(f"If you need help, please refer to the the GitHub Repository for the [User Manual]({user_manual_link}).")
|
|
|
st.stop()
|
|
|
|
|
|
|
|
|
refresh_button = st.sidebar.button("Refresh Data")
|
|
|
|
|
|
|
|
|
channel_details, videos, all_video_data, videos_df = download_data(st.session_state.API_KEY, st.session_state.CHANNEL_ID)
|
|
|
|
|
|
if channel_details is None:
|
|
|
st.warning("Invalid YouTube Channel ID. Please check and enter a valid Channel ID.")
|
|
|
st.stop()
|
|
|
|
|
|
if refresh_button:
|
|
|
with st.spinner("Refreshing data..."):
|
|
|
channel_details, videos, all_video_data, videos_df = download_data(st.session_state.API_KEY, st.session_state.CHANNEL_ID)
|
|
|
|
|
|
if channel_details is None:
|
|
|
st.warning("Invalid YouTube Channel ID. Please check and enter a valid Channel ID.")
|
|
|
st.stop()
|
|
|
|
|
|
|
|
|
st.sidebar.title("Data Filters")
|
|
|
|
|
|
num_videos = st.sidebar.slider("Select Number of Top Videos to Display:", 1, 50, 10)
|
|
|
|
|
|
|
|
|
all_video_data['published_date'] = pd.to_datetime(all_video_data['published_date'])
|
|
|
|
|
|
|
|
|
min_date = all_video_data['published_date'].min().date()
|
|
|
max_date = all_video_data['published_date'].max().date()
|
|
|
|
|
|
|
|
|
start_date = st.sidebar.date_input("Select Start Date", min_date)
|
|
|
end_date = st.sidebar.date_input("Select End Date", max_date)
|
|
|
|
|
|
if start_date > end_date:
|
|
|
st.sidebar.warning("Start date should be earlier than end date.")
|
|
|
st.stop()
|
|
|
|
|
|
tag_search = st.sidebar.text_input("Search Videos by Tag")
|
|
|
|
|
|
date_range_start = pd.Timestamp(start_date)
|
|
|
date_range_end = pd.Timestamp(end_date)
|
|
|
|
|
|
filtered_data = all_video_data[(all_video_data['published_date'] >= date_range_start) &
|
|
|
(all_video_data['published_date'] <= date_range_end)]
|
|
|
|
|
|
if tag_search:
|
|
|
filtered_data = filtered_data[filtered_data['tags'].apply(lambda x: tag_search in x)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("Channel Details", divider="green")
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
|
|
with col1:
|
|
|
channel_thumbnail = channel_details['thumbnail']
|
|
|
|
|
|
add_logo(channel_thumbnail, height=300)
|
|
|
|
|
|
view_count = int(channel_details['viewCount'])
|
|
|
subscriber_count = int(channel_details['subscriberCount'])
|
|
|
|
|
|
|
|
|
view_count_formatted = "{:,}".format(view_count)
|
|
|
subscriber_count_formatted = "{:,}".format(subscriber_count)
|
|
|
|
|
|
st.markdown(f"**Channel Title:** {channel_details['title']}")
|
|
|
st.markdown(f"**Channel Description:** {channel_details['description']}")
|
|
|
|
|
|
with col3:
|
|
|
|
|
|
st.link_button("Go to Channel", f"https://www.youtube.com/channel/{st.session_state.CHANNEL_ID}")
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
col1.metric("Total Views", view_count_formatted, "")
|
|
|
col2.metric("Subscribers", subscriber_count_formatted, "")
|
|
|
col3.metric("Total Videos", len(videos), "")
|
|
|
style_metric_cards(background_color="#000000",
|
|
|
border_left_color="#049204",
|
|
|
border_color="#0E0E0E"
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
|
|
|
|
with col1:
|
|
|
st.subheader(f"Top {num_videos} Videos Based on Views")
|
|
|
sorted_video_data = filtered_data.sort_values(by='view_count', ascending=False)
|
|
|
|
|
|
top_views_df = sorted_video_data.head(num_videos)
|
|
|
with chart_container(top_views_df):
|
|
|
|
|
|
|
|
|
fig = px.bar(top_views_df, x='title', y='view_count')
|
|
|
|
|
|
fig.update_layout(xaxis_title="Video Title",
|
|
|
yaxis_title="View Count")
|
|
|
fig.update_traces(marker_color='green')
|
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
with col2:
|
|
|
st.subheader(f"Top {num_videos} Videos Based on Likes")
|
|
|
sorted_video_data = filtered_data.sort_values(by='like_count', ascending=False)
|
|
|
|
|
|
top_likes_df = sorted_video_data.head(num_videos)
|
|
|
|
|
|
with chart_container(top_likes_df):
|
|
|
|
|
|
|
|
|
fig = px.bar(top_likes_df, x='title', y='like_count')
|
|
|
|
|
|
fig.update_layout(xaxis_title="Video Title",
|
|
|
yaxis_title="Like Count")
|
|
|
fig.update_traces(marker_color='orange')
|
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
with col3:
|
|
|
st.subheader(f"Top {num_videos} Based on Comments")
|
|
|
sorted_video_data = filtered_data.sort_values(by='comment_count', ascending=False)
|
|
|
|
|
|
top_comments_df = sorted_video_data.head(num_videos)
|
|
|
with chart_container(top_comments_df):
|
|
|
|
|
|
|
|
|
fig = px.bar(top_comments_df, x='title', y='comment_count')
|
|
|
|
|
|
fig.update_layout(xaxis_title="Video Title",
|
|
|
yaxis_title="Comment Count")
|
|
|
fig.update_traces(marker_color='green')
|
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.subheader("Viewership Growth Over Time", divider="green")
|
|
|
views = filtered_data['view_count']
|
|
|
dates = filtered_data['published_date']
|
|
|
|
|
|
|
|
|
fig = go.Figure()
|
|
|
|
|
|
fig.add_trace(
|
|
|
go.Scatter(x=dates, y=views, mode='lines+markers', name='Views Over Time', line=dict(color='orange'))
|
|
|
)
|
|
|
|
|
|
fig.update_layout(title='Views Over Time',
|
|
|
xaxis_title='Published Date',
|
|
|
yaxis_title='Number of Views',
|
|
|
template="plotly_dark")
|
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.subheader("Predicted Viewership Growth Over Time", divider="green")
|
|
|
|
|
|
with st.spinner("Predicting Views for the next Week"):
|
|
|
|
|
|
forecast_df = all_video_data[['published_date', 'view_count']]
|
|
|
forecast_df.columns = ['ds', 'y']
|
|
|
|
|
|
|
|
|
model = Prophet(
|
|
|
yearly_seasonality=False,
|
|
|
weekly_seasonality=True,
|
|
|
daily_seasonality=True,
|
|
|
seasonality_mode='additive')
|
|
|
|
|
|
|
|
|
model.fit(forecast_df)
|
|
|
|
|
|
|
|
|
future_dates = model.make_future_dataframe(periods=30)
|
|
|
|
|
|
|
|
|
forecast = model.predict(future_dates)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
forecasted_period = forecast[forecast['ds'] > forecast_df['ds'].max()]
|
|
|
|
|
|
|
|
|
|
|
|
forecasted_period = forecast[forecast['ds'] > forecast_df['ds'].max()]
|
|
|
|
|
|
|
|
|
last_date = forecast_df['ds'].max()
|
|
|
start_date = last_date - datetime.timedelta(days=30)
|
|
|
last_30_days = forecast_df[(forecast_df['ds'] > start_date) & (forecast_df['ds'] <= last_date)]
|
|
|
|
|
|
|
|
|
trace1 = go.Scatter(x=last_30_days['ds'], y=last_30_days['y'], mode='lines', name='Actual Views (Last 30 Days)')
|
|
|
trace2 = go.Scatter(x=forecasted_period['ds'], y=forecasted_period['yhat'], mode='lines',
|
|
|
name='Predicted Views (Next 30 Days)')
|
|
|
layout = go.Layout(title="YouTube Views: Last 30 Days and Forecast for Next 30 Days", xaxis_title="Date",
|
|
|
yaxis_title="Views")
|
|
|
fig = go.Figure(data=[trace1, trace2], layout=layout)
|
|
|
|
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
|
|
with col1:
|
|
|
st.divider()
|
|
|
with st.spinner("Generating Word Cloud..."):
|
|
|
st.subheader("Most Common Tags")
|
|
|
|
|
|
all_tags = " ".join(" ".join(tags) for tags in filtered_data['tags'])
|
|
|
|
|
|
|
|
|
wordcloud = WordCloud(width=800, height=400, background_color='black').generate(all_tags)
|
|
|
|
|
|
|
|
|
plt.figure(figsize=(10, 5))
|
|
|
plt.imshow(wordcloud, interpolation='bilinear')
|
|
|
plt.axis('off')
|
|
|
plt.tight_layout(pad=0)
|
|
|
|
|
|
|
|
|
buf = io.BytesIO()
|
|
|
plt.savefig(buf, format="png", bbox_inches='tight', pad_inches=0)
|
|
|
buf.seek(0)
|
|
|
|
|
|
st.image(buf, use_column_width=True)
|
|
|
|
|
|
with col2:
|
|
|
|
|
|
filtered_data['like_to_view_ratio'] = filtered_data['like_count'] / filtered_data['view_count']
|
|
|
|
|
|
|
|
|
like_to_view_ratio = filtered_data['like_to_view_ratio']
|
|
|
|
|
|
st.divider()
|
|
|
st.subheader("Like-to-View Ratio Over Time")
|
|
|
|
|
|
|
|
|
fig_ratio = go.Figure()
|
|
|
|
|
|
fig_ratio.add_trace(go.Scatter(x=dates, y=like_to_view_ratio, mode='lines+markers', name='Like-to-View Ratio',
|
|
|
line=dict(color='green')))
|
|
|
|
|
|
fig_ratio.update_layout(xaxis_title='Published Date',
|
|
|
yaxis_title='Like-to-View Ratio',
|
|
|
template="plotly_dark")
|
|
|
|
|
|
|
|
|
st.plotly_chart(fig_ratio, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.divider()
|
|
|
st.subheader("Detailed Video Statistics Video Selection")
|
|
|
st.write("Click on view statistics to get detailed information related to the selected video")
|
|
|
|
|
|
display_video_list(videos, 0, 10)
|
|
|
|