413 lines
12 KiB
HTML
413 lines
12 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
|
|
<title>TubeStats</title>
|
|
|
|
|
|
<link rel="stylesheet" href="dist/reset.css">
|
|
<link rel="stylesheet" href="dist/reveal.css">
|
|
<link rel="stylesheet" href="dist/theme/simple.css" id="theme">
|
|
|
|
<!-- Theme used for syntax highlighted code -->
|
|
<link rel="stylesheet" href="plugin/highlight/monokai.css" id="highlight-theme">
|
|
</head>
|
|
<body>
|
|
<div class="reveal">
|
|
<div class="slides">
|
|
<section data-markdown>
|
|
# TubeStats
|
|
*A hobby project: Consistency in a YouTube channel*
|
|
|
|
Shivan Sivakumaran
|
|
</section>
|
|
<section data-markdown>
|
|
## Inspiration
|
|
- Ali Abdaal
|
|
- Consistency - how consistent?
|
|
- Getting better as a beginner
|
|
- www.tubestats.app
|
|
</section>
|
|
<section data-markdown>
|
|
## What does TubeStats do?
|
|
1. Takes user input
|
|
2. Provides statistics
|
|
</section>
|
|
<section data-background-image="tubestats_parsing.gif"
|
|
data-background-size="750px">
|
|
</section>
|
|
<section data-markdown>
|
|
## 1. User input
|
|
```python
|
|
# Channel ID
|
|
'UCoOae5nYA7VqaXzerajD0lg'
|
|
# Link to channel
|
|
'https://www.youtube.com/channel/UCoOae5nYA7VqaXzerajD0lg'
|
|
# Link to video
|
|
'https://www.youtube.com/watch?v=epF2SYpWtos'
|
|
# Video ID
|
|
'epF2SYpWtos'
|
|
```
|
|
</section>
|
|
<section data-markdown>
|
|
## 2. Statistics
|
|
![](all-graph.png)
|
|
</section>
|
|
<section data-markdown>
|
|
![](time-diff.png)
|
|
</section>
|
|
<section data-markdown>
|
|
## How does TubeStats work?
|
|
### Part 1 of 2
|
|
1. How to set up a development environment?
|
|
2. How to access the video information?
|
|
3. How to store password and API keys?
|
|
4. How do we get and store the video statistics?
|
|
</section>
|
|
<section data-markdown>
|
|
## 1. Development environment
|
|
```bash
|
|
$ mkdir tubestats
|
|
$ cd tubestats
|
|
|
|
$ python3 -m venv venv
|
|
$ source venv/bin/activate
|
|
$ (venv)
|
|
|
|
$ git init
|
|
```
|
|
</section>
|
|
<section data-markdown>
|
|
## 2. Video information
|
|
- use `beautifulsoup`, `scraPY`, `selenium`?
|
|
- YouTube Data API
|
|
- `google-api-python-client`
|
|
</section>
|
|
<section data-markdown>
|
|
## 3. Storing API Keys
|
|
- Hard code?
|
|
- `python-dotenv`
|
|
- `.env`
|
|
- `.gitignore`
|
|
</section>
|
|
<section data-markdown>
|
|
### 3. Storing API keys
|
|
```
|
|
# .env
|
|
|
|
API_KEY=xxxxxxxx
|
|
```
|
|
|
|
```python
|
|
# tubestats/youtube_api.py
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
API_KEY = os.getenv('API_KEY')
|
|
|
|
```
|
|
</section>
|
|
<section data-markdown>
|
|
## 4. Get YouTube video statistics
|
|
- Access API
|
|
- Channel upload playlist
|
|
- Video statistics
|
|
- `pandas` dataframe
|
|
</section>
|
|
<section data-markdown>
|
|
### 4. Get YouTube video statistics
|
|
```python
|
|
import googleapiclient.discovery
|
|
|
|
load_dotenv()
|
|
api_service_name = 'youtube'
|
|
api_version = 'v3'
|
|
|
|
youtube = googleapiclient.discovery.build(
|
|
api_service_name,
|
|
api_version,
|
|
developerKey=os.getenv('API_KEY'))
|
|
```
|
|
|
|
</section>
|
|
<section data-markdown>
|
|
```python [|3|5-16|17-18|20-29|30-32]
|
|
# tubestates/youtube_api.py
|
|
|
|
upload_playlist_ID = channel_data['upload_playlist_ID']
|
|
|
|
video_response = []
|
|
next_page_token = None
|
|
while True:
|
|
# obtaining video ID + titles
|
|
playlist_request = youtube.playlistItems().list(
|
|
part='snippet,contentDetails',
|
|
maxResults=50, # API Limit is 50
|
|
pageToken=next_page_token,
|
|
playlistId=upload_playlist_ID,
|
|
)
|
|
playlist_response = playlist_request.execute()
|
|
# isolating video ID
|
|
vid_subset = [ vid_ID['contentDetails']['videoId']
|
|
for vid_ID in playlist_response['items'] ]
|
|
# retrieving video statistics
|
|
vid_info_subset_request = youtube.videos().list(
|
|
part='snippet,contentDetails,statistics',
|
|
id=vid_subset
|
|
)
|
|
vid_info_subset_response = vid_info_subset_request.execute()
|
|
video_response.append(vid_info_subset_response)
|
|
# obtaining page token
|
|
next_page_token = playlist_response.get('nextPageToken') # get method used because token may not exist
|
|
if next_page_token is None:
|
|
break
|
|
|
|
df = pd.json_normalize(video_response, 'items')
|
|
return df
|
|
</section>
|
|
<section data-markdown>
|
|
### Video statistics
|
|
![](dataframe.png)
|
|
</section>
|
|
<section data-markdown>
|
|
## How does TubeStats work?
|
|
### Part 2 of 2
|
|
5. How to organise the code?
|
|
6. How to test the code?
|
|
7. How to display the data and allow interaction?
|
|
8. How to account for variable input?
|
|
</section>
|
|
<section data-markdown>
|
|
## 5. Organising code
|
|
```bash [1-9|10-15|16-20|21]
|
|
tubestats/
|
|
├── data
|
|
│ ├── channel_data.pkl
|
|
│ └── video_data.pkl
|
|
├── LICENSE
|
|
├── Procfile
|
|
├── README.MD
|
|
├── requirements.txt
|
|
├── setup.sh
|
|
├── tests
|
|
│ ├── __init__.py
|
|
│ ├── test_settings.py
|
|
│ ├── test_youtube_api.py
|
|
│ ├── test_youtube_data.py
|
|
│ └── test_youtube_parser.py
|
|
├── tubestats
|
|
│ ├── __init__.py
|
|
│ ├── youtube_api.py
|
|
│ ├── youtube_data.py
|
|
│ └── youtube_parser.py
|
|
└── youtube_presenter.py
|
|
</section>
|
|
<section data-markdown>
|
|
## 6. Testing
|
|
```python [|15-20]
|
|
# tests/tests_youtube_api.py
|
|
from tubestats.youtube_api import create_api, YouTubeAPI
|
|
from tests.test_settings import set_channel_ID_test_case
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import googleapiclient
|
|
import pandas
|
|
|
|
def test_create_api():
|
|
youtube = create_api()
|
|
assert isinstance(youtube, googleapiclient.discovery.Resource)
|
|
|
|
@pytest.fixture()
|
|
def youtubeapi():
|
|
channel_ID = set_channel_ID_test_case()
|
|
yt = YouTubeAPI(channel_ID)
|
|
return yt
|
|
|
|
def test_get_video_data(youtubeapi):
|
|
df = youtubeapi.get_video_data()
|
|
assert isinstance(df, pandas.core.frame.DataFrame)
|
|
|
|
# saving video data to save API calls for later testing
|
|
BASE_DIR = Path(__file__).parent.parent
|
|
df.to_pickle(BASE_DIR / 'data' / 'video_data.pkl')
|
|
</section>
|
|
<section data-markdown>
|
|
## 7. Sharing to the world
|
|
- graphs with tool tips, `altair`
|
|
- creating interaction with `streamlit`
|
|
- hosting on Heroku
|
|
</section>
|
|
<section data-markdown>
|
|
### 7. Sharing to the world
|
|
```python []
|
|
# tubestats/youtube_data.py
|
|
|
|
import altair as alt
|
|
|
|
def scatter_all_videos(self, df: pd.core.frame.DataFrame) -> alt.vegalite.v4.Chart:
|
|
df_views = df
|
|
c = alt.Chart(df_views, title='Plot of videos over time').mark_point().encode(
|
|
x=alt.X('snippet\.publishedAt_REFORMATED:T', axis=alt.Axis(title='Date Published'), scale=alt.Scale(type='log')),
|
|
y=alt.Y('statistics\.viewCount:Q', axis=alt.Axis(title='View Count')),
|
|
color=alt.Color('statistics\.like-dislike-ratio:Q', scale=alt.Scale(scheme='turbo'), legend=None),
|
|
tooltip=['snippet\.title:N', 'statistics\.viewCount:Q', 'statistics\.like-dislike-ratio:Q'],
|
|
size=alt.Size('statistics\.viewCount:Q', legend=None)
|
|
)
|
|
return c
|
|
</section>
|
|
<section data-markdown>
|
|
### 7. Sharing to the world
|
|
```python [|5-14|16-19]
|
|
# youtube_presenter.py
|
|
|
|
import streamlit as st
|
|
|
|
def date_slider(date_end=datetime.today()):
|
|
date_start, date_end = st.slider(
|
|
'Select date range to include:',
|
|
min_value=first_video_date, # first video
|
|
max_value=last_video_date, #value for date_end
|
|
value=(first_video_date , last_video_date), #same as min value
|
|
step=timedelta(days=2),
|
|
format='YYYY-MM-DD',
|
|
key=999)
|
|
return date_start, date_end
|
|
|
|
date_start, date_end = date_slider()
|
|
transformed_df = youtuber_data.transform_dataframe(date_start=date_start, date_end=date_end)
|
|
c = youtuber_data.scatter_all_videos(transformed_df)
|
|
st.altair_chart(c, use_container_width=True)
|
|
</section>
|
|
<section data-markdown>
|
|
### 7. Sharing with the world
|
|
```shell []
|
|
$ streamlit run youtube_presenter.py
|
|
|
|
You can now view your Streamlit app in your browser.
|
|
|
|
Local URL: http://localhost:8501
|
|
Network URL: http://192.0.0.0.1
|
|
</section>
|
|
<section data-markdown>
|
|
![](all-graph.png)
|
|
</section>
|
|
<section data-markdown>
|
|
### 7. Sharing to the world
|
|
```bash
|
|
$ (venv) pip freeze > requirements.txt
|
|
```
|
|
|
|
```bash
|
|
# setup.sh
|
|
|
|
mkdir -p ~/.streamlit/echo "\
|
|
[server]\n\
|
|
headless = true\n\
|
|
port = $PORT\n\
|
|
enableCORS = false\n\
|
|
\n\
|
|
" > ~/.streamlit/config.toml
|
|
```
|
|
|
|
```bash
|
|
# Procfile
|
|
|
|
web: sh setup.sh && streamlit run youtube_presenter.py
|
|
```
|
|
|
|
```bash
|
|
$ heroku login
|
|
$ heroku create tubestats
|
|
|
|
$ git push heroku main
|
|
```
|
|
</section>
|
|
<section data-markdown>
|
|
## 8. Different user input
|
|
- taking video ID
|
|
- URL links
|
|
- using regex, `re` module
|
|
</section>
|
|
<section data-markdown>
|
|
### 8. Different user input
|
|
```python []
|
|
import re
|
|
|
|
LINK_MATCH = r'(^.*youtu)(\.be|be\.com)(\/watch\?v\=|\/)([a-zA-Z0-9_-]+)(\/)?([a-zA-Z0-9_-]+)?'
|
|
m = re.search(LINK_MATCH, for_parse)
|
|
video_id = m.group(4) # video ID
|
|
if video_id == 'channel':
|
|
return m.group(6) # Channel ID
|
|
elif video_id == 'user':
|
|
channel_username = m.group(6) # Channel Username
|
|
</section>
|
|
<section data-markdown>
|
|
### 8. Different user input
|
|
![](regex.png)
|
|
</section>
|
|
<section data-markdown>
|
|
## Somethings I would like to discuss
|
|
- DataFrame and memory
|
|
- Error handling
|
|
- Async?
|
|
</section>
|
|
<section data-markdown>
|
|
### DataFrame immutability and memory?
|
|
```python []
|
|
df = self.df
|
|
df = df[['snippet.publishedAt',
|
|
'snippet.title',
|
|
...
|
|
'statistics.favoriteCount',
|
|
'statistics.commentCount']]
|
|
|
|
df = df.fillna(0)
|
|
|
|
df = df.astype({'statistics.viewCount': 'int',
|
|
...
|
|
'statistics.commentCount': 'int',})
|
|
df['statistics.viewCount_NLOG'] = df['statistics.viewCount'].apply(lambda x : np.log(x))
|
|
|
|
df = df.sort_values(by='snippet.publishedAt_REFORMATED', ascending=True)
|
|
</section>
|
|
<section data-markdown>
|
|
## What did I learn
|
|
- Project based learning
|
|
- 'minimal viable product'
|
|
</section>
|
|
<section data-markdown>
|
|
## Conclusion
|
|
- Analysing consistency
|
|
- YouTube Data API --> Heroku
|
|
- Share your work!
|
|
</section>
|
|
<section data-markdown>
|
|
## Acknowledgements
|
|
- Menno Finlay-Smits
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script src="dist/reveal.js"></script>
|
|
<script src="plugin/notes/notes.js"></script>
|
|
<script src="plugin/markdown/markdown.js"></script>
|
|
<script src="plugin/highlight/highlight.js"></script>
|
|
<script>
|
|
// More info about initialization & config:
|
|
// - https://revealjs.com/initialization/
|
|
// - https://revealjs.com/config/
|
|
Reveal.initialize({
|
|
hash: true,
|
|
|
|
// Learn about plugins: https://revealjs.com/plugins/
|
|
plugins: [ RevealMarkdown, RevealHighlight, RevealNotes ]
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|