Skip to content

Commit f737ace

Browse files
committed
Upload project
Upload project Upload Project
1 parent 069f48d commit f737ace

File tree

10 files changed

+559
-0
lines changed

10 files changed

+559
-0
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

Procfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
worker: python checker.py

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Youtube FIrst Commenter Bot
2+
3+
A bot that takes a list of youtube channels and posts the first comment in every new video.
4+
5+
## Getting Started
6+
7+
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.
8+
9+
### Prerequisites
10+
11+
1. Use Google API Console to create OAuth 2.0 credentials:
12+
1. Visit the [developer console](https://console.developers.google.com)
13+
1. Create a new project
14+
1. Open the [API Manager](https://console.developers.google.com/apis/)
15+
1. Enable *YouTube Data API v3*
16+
1. Go to [Credentials](https://console.developers.google.com/apis/credentials)
17+
1. Configure the OAuth consent screen and create *OAuth client ID* credentials
18+
1. Use Application Type *Other* and provide a client name (e.g. *Python*)
19+
1. Confirm and download the generated credentials as JSON file
20+
1. Store the file in the application folder as *keys/client_secrets.json*
21+
22+
23+
### Installing
24+
25+
Installing the requirements
26+
27+
```
28+
pip install -r requirements.txt
29+
```
30+
31+
Create a database named email with the following structure (I suggest using the free-tier Amazon RDS):
32+
33+
+--------------+--------------+------+-----+
34+
| Field | Type | Null | Key |
35+
+--------------+--------------+------+-----+
36+
| id | varchar(100) | NO | PRI |
37+
| username | varchar(100) | NO | UNI |
38+
| title | varchar(100) | YES | |
39+
| added_on | varchar(100) | NO | |
40+
| last_checked | varchar(100) | NO | |
41+
+--------------+--------------+------+-----+
42+
43+
You will also need to add your information as follows:
44+
45+
checker.py
46+
47+
store = DataStore('username', 'passw', 'host', 'dbname') # Your db credentials - line 60
48+
49+
youtubeapi.py
50+
51+
CLIENT_SECRETS_FILE = "keys/client_secrets.json" # The location of the secrets file - line 19
52+
CLIENT_ID = "Your Client Id" # line 20
53+
CLIENT_SECRET = "Your Client Secret" # line 21
54+
55+
commenter.py
56+
57+
f.write("First Comment!") # Default Comment to add when no comments file exists for this channel - line 80
58+
59+
Lastly, run `python3 checker.py -i CHANNEL_ID add` or `python3 checker.py -u CHANNEL_NAME add` to add the Youtube Channels you want
60+
and go to */comments* and create a *CHANNEL_NAME_comments.txt* for each channel containing a comment in each row.
61+
You can also let the script create the comments files with the default comment you specified and modify them later.
62+
63+
And your are good to go!
64+
65+
Run `python3 checker.py list` to see the Youtube Channels list, ,
66+
`python3 checker.py -i CHANNEL_ID remove` or `python3 checker.py -u CHANNEL_NAME remove` to remove a Youtube Channel and
67+
`python3 checker.py` to run continuesly the script.
68+
69+
### Note: The first time a browser window should open asking for confirmation. The next times, it will connect automatically.
70+
71+
## Deployment
72+
73+
You can easily deploy it on heroku.com (Procfile will automatically run the script).
74+
75+
## License
76+
77+
This project is licensed under the GNU General Public License v3.0 License

checker.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/env python3
2+
import sys
3+
import argparse
4+
import arrow
5+
import time
6+
from youtubeapi import YouTube
7+
from datastore import DataStore
8+
from commenter import Comment
9+
10+
def print_from_youtube(headers, data):
11+
"""Print the provided header and data in a visually pleasing manner
12+
13+
Args:
14+
headers (list of str): The headers to print
15+
data (list of list of str): The data rows
16+
"""
17+
18+
if (len(data) == 0):
19+
return
20+
separators = []
21+
for word in headers:
22+
separators.append('-' * len(word))
23+
output = [headers, separators] + data
24+
col_widths = [0] * len(headers)
25+
for row in output:
26+
for idx, column in enumerate(row):
27+
if len(column) > col_widths[idx]:
28+
col_widths[idx] = len(column)
29+
for row in output:
30+
for idx, column in enumerate(row):
31+
print("".join(column.ljust(col_widths[idx])), end = ' ' * 2)
32+
print()
33+
34+
def print_from_database(store):
35+
print("{}".format("_"*186))
36+
print("|{:-^30}|{:-^30}|{:-^60}|{:-^30}|{:-^30}|".format('ID', 'Username', 'Title', 'Added On', 'Last Checked'))
37+
for item in store.get_channels():
38+
print("|{:^30}|{:^30}|{:^60}|{:^30}|{:^30}|".format(item['id'], item['username'], str(item['title']), arrow.get(item['added_on']).humanize(), arrow.get(item['last_checked']).humanize()))
39+
print("|{}|".format("_"*184))
40+
41+
def get_parser():
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument('-i', '--id', help = 'Channel ID', default = None)
44+
parser.add_argument('-u', '--username', help = 'Username', default = None)
45+
parser.add_argument('action', help = 'Perform the specified action', default = 'check',
46+
nargs = '?', choices = ['add', 'check', 'list', 'remove'])
47+
return parser
48+
49+
def main():
50+
"""Parse the command line arguments, expecting one of the following formats:
51+
-) (-i ChannelID | -u Username) (add | check | remove)
52+
-) check | list
53+
and perform the appropriate action
54+
"""
55+
print("Starting..")
56+
parser = get_parser()
57+
args = parser.parse_args()
58+
59+
youtube = YouTube()
60+
store = DataStore('username', 'passw', 'host', 'dbname') # Your db credentials
61+
62+
channel = None
63+
if args.username is not None:
64+
channel = youtube.get_channel_by_username(args.username)
65+
elif args.id is not None:
66+
channel = youtube.get_channel_by_id(args.id)
67+
68+
if args.action == 'add':
69+
store.store_channel(channel)
70+
elif args.action == 'remove':
71+
store.remove_channel(channel)
72+
elif args.action == 'list':
73+
print_from_database(store)
74+
elif args.action == 'check':
75+
print("Done! Waiting for new videos to be uploaded..")
76+
while True:
77+
# If the user passed a specific channel, check for new uploads
78+
# otherwhise check for uploads from every previously added channel
79+
channels = []
80+
if channel is not None:
81+
channels.append(store.get_channel_by_id(channel['id']))
82+
else:
83+
channels = store.get_channels()
84+
85+
data = []
86+
to_check = dict()
87+
for channel_item in channels:
88+
to_check[channel_item['id']] = channel_item['last_checked']
89+
90+
uploads = youtube.get_uploads(to_check)
91+
try:
92+
for upload in uploads:
93+
current_link = 'https://youtube.com/watch?v=%s' % (upload['id'], )
94+
data.append([
95+
upload['channel_title'],
96+
upload['title'],
97+
arrow.get(upload['published_at']).humanize(),
98+
current_link
99+
])
100+
Comment(youtube.api, upload['id'], upload['channel_title'])
101+
102+
print_from_youtube(['Channel', 'Title', 'Published', 'Link'], data)
103+
104+
for channel_id in to_check.keys():
105+
store.update_last_checked(channel_id)
106+
107+
# Look for new videos every 15 seconds
108+
time.sleep(15)
109+
except BaseException as be:
110+
# If it reaches the 100 seconds api threshold, wait for 100 seconds
111+
print("Error: Too many requests:\n{}".format(be))
112+
print("Waiting 100 seconds..")
113+
time.sleep(100)
114+
print("Waiting for new videos to be uploaded..")
115+
116+
if __name__ == '__main__':
117+
main()

commenter.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import os
4+
import google.oauth2.credentials
5+
import requests
6+
from bs4 import BeautifulSoup
7+
import random
8+
import google_auth_oauthlib.flow
9+
from googleapiclient.discovery import build
10+
from googleapiclient.errors import HttpError
11+
from google_auth_oauthlib.flow import InstalledAppFlow
12+
import webbrowser
13+
from oauth2client.client import OAuth2WebServerFlow
14+
from oauth2client import tools
15+
from oauth2client.file import Storage
16+
17+
18+
# Build a resource based on a list of properties given as key-value pairs.
19+
# Leave properties with empty values out of the inserted resource.
20+
def build_resource(properties):
21+
resource = {}
22+
for p in properties:
23+
# Given a key like "snippet.title", split into "snippet" and "title", where
24+
# "snippet" will be an object and "title" will be a property in that object.
25+
prop_array = p.split('.')
26+
ref = resource
27+
for pa in range(0, len(prop_array)):
28+
is_array = False
29+
key = prop_array[pa]
30+
# For properties that have array values, convert a name like
31+
# "snippet.tags[]" to snippet.tags, and set a flag to handle
32+
# the value as an array.
33+
if key[-2:] == '[]':
34+
key = key[0:len(key)-2:]
35+
is_array = True
36+
if pa == (len(prop_array) - 1):
37+
# Leave properties without values out of inserted resource.
38+
if properties[p]:
39+
if is_array:
40+
ref[key] = properties[p].split(',')
41+
else:
42+
ref[key] = properties[p]
43+
elif key not in ref:
44+
# For example, the property is "snippet.title", but the resource does
45+
# not yet have a "snippet" object. Create the snippet object here.
46+
# Setting "ref = ref[key]" means that in the next time through the
47+
# "for pa in range ..." loop, we will be setting a property in the
48+
# resource's "snippet" object.
49+
ref[key] = {}
50+
ref = ref[key]
51+
else:
52+
# For example, the property is "snippet.description", and the resource
53+
# already has a "snippet" object.
54+
ref = ref[key]
55+
return resource
56+
57+
# Remove keyword arguments that are not set
58+
def remove_empty_kwargs(**kwargs):
59+
good_kwargs = {}
60+
if kwargs is not None:
61+
for key, value in kwargs.items():
62+
if value:
63+
good_kwargs[key] = value
64+
return good_kwargs
65+
66+
67+
def comment_threads_insert(client, properties, **kwargs):
68+
# Add the comment
69+
resource = build_resource(properties)
70+
kwargs = remove_empty_kwargs(**kwargs)
71+
response = client.commentThreads().insert(body=resource,**kwargs).execute()
72+
return True
73+
74+
75+
def Comment(api, video_id, channel_title):
76+
file_path = 'comments/{}_comments.txt'.format(channel_title)
77+
# If comments file for this channel doesn't exist, create it and add default comment.
78+
if not os.path.exists(file_path):
79+
f = open(file_path, 'w', encoding="ISO-8859-1")
80+
f.write("First Comment!") # Default Comment to add when no comments file exists for this channel
81+
f.close()
82+
83+
# Take a comment at random and post it!
84+
with open(file_path, 'r', encoding="ISO-8859-1") as f:
85+
comments_list = [line.strip() for line in f]
86+
try:
87+
comment_text = random.choice(comments_list)
88+
print("\n\nNew Video!")
89+
print("Comment to add: ", comment_text)
90+
comment_threads_insert(api,
91+
{'snippet.channelId': 'UC7HIr-gmYyPJvGjKO0A6t5w',
92+
'snippet.videoId': video_id,
93+
'snippet.topLevelComment.snippet.textOriginal': comment_text},
94+
part='snippet')
95+
print("Comment added.")
96+
except BaseException as bs:
97+
print("An error occured:")
98+
print(bs)
99+
print("Video Details: ")
100+

comments/ADD YOUR COMMENTS HERE

Whitespace-only changes.

0 commit comments

Comments
 (0)