aboutsummaryrefslogtreecommitdiffstats
path: root/youtube/subscriptions.py
diff options
context:
space:
mode:
Diffstat (limited to 'youtube/subscriptions.py')
-rw-r--r--youtube/subscriptions.py121
1 files changed, 68 insertions, 53 deletions
diff --git a/youtube/subscriptions.py b/youtube/subscriptions.py
index 6f75578..b841f5d 100644
--- a/youtube/subscriptions.py
+++ b/youtube/subscriptions.py
@@ -26,6 +26,7 @@ thumbnails_directory = os.path.join(settings.data_dir, "subscription_thumbnails"
database_path = os.path.join(settings.data_dir, "subscriptions.sqlite")
+
def open_database():
if not os.path.exists(settings.data_dir):
os.makedirs(settings.data_dir)
@@ -74,11 +75,13 @@ def open_database():
# https://stackoverflow.com/questions/19522505/using-sqlite3-in-python-with-with-keyword
return contextlib.closing(connection)
+
def with_open_db(function, *args, **kwargs):
with open_database() as connection:
with connection as cursor:
return function(cursor, *args, **kwargs)
+
def _is_subscribed(cursor, channel_id):
result = cursor.execute('''SELECT EXISTS(
SELECT 1
@@ -88,12 +91,14 @@ def _is_subscribed(cursor, channel_id):
)''', [channel_id]).fetchone()
return bool(result[0])
+
def is_subscribed(channel_id):
if not os.path.exists(database_path):
return False
return with_open_db(_is_subscribed, channel_id)
+
def _subscribe(channels):
''' channels is a list of (channel_id, channel_name) '''
channels = list(channels)
@@ -101,7 +106,8 @@ def _subscribe(channels):
with connection as cursor:
channel_ids_to_check = [channel[0] for channel in channels if not _is_subscribed(cursor, channel[0])]
- rows = ( (channel_id, channel_name, 0, 0) for channel_id, channel_name in channels)
+ rows = ((channel_id, channel_name, 0, 0) for channel_id,
+ channel_name in channels)
cursor.executemany('''INSERT OR IGNORE INTO subscribed_channels (yt_channel_id, channel_name, time_last_checked, next_check_time)
VALUES (?, ?, ?, ?)''', rows)
@@ -111,6 +117,7 @@ def _subscribe(channels):
channel_names.update(channels)
check_channels_if_necessary(channel_ids_to_check)
+
def delete_thumbnails(to_delete):
for thumbnail in to_delete:
try:
@@ -122,6 +129,7 @@ def delete_thumbnails(to_delete):
print('Failed to delete thumbnail: ' + thumbnail)
traceback.print_exc()
+
def _unsubscribe(cursor, channel_ids):
''' channel_ids is a list of channel_ids '''
to_delete = []
@@ -138,7 +146,8 @@ def _unsubscribe(cursor, channel_ids):
gevent.spawn(delete_thumbnails, to_delete)
cursor.executemany("DELETE FROM subscribed_channels WHERE yt_channel_id=?", ((channel_id, ) for channel_id in channel_ids))
-def _get_videos(cursor, number_per_page, offset, tag = None):
+
+def _get_videos(cursor, number_per_page, offset, tag=None):
'''Returns a full page of videos with an offset, and a value good enough to be used as the total number of videos'''
# We ask for the next 9 pages from the database
# Then the actual length of the results tell us if there are more than 9 pages left, and if not, how many there actually are
@@ -181,8 +190,6 @@ def _get_videos(cursor, number_per_page, offset, tag = None):
return videos, pseudo_number_of_videos
-
-
def _get_subscribed_channels(cursor):
for item in cursor.execute('''SELECT channel_name, yt_channel_id, muted
FROM subscribed_channels
@@ -204,7 +211,6 @@ def _remove_tags(cursor, channel_ids, tags):
)''', pairs)
-
def _get_tags(cursor, channel_id):
return [row[0] for row in cursor.execute('''SELECT tag
FROM tag_associations
@@ -212,9 +218,11 @@ def _get_tags(cursor, channel_id):
SELECT id FROM subscribed_channels WHERE yt_channel_id = ?
)''', (channel_id,))]
+
def _get_all_tags(cursor):
return [row[0] for row in cursor.execute('''SELECT DISTINCT tag FROM tag_associations''')]
+
def _get_channel_names(cursor, channel_ids):
''' returns list of (channel_id, channel_name) '''
result = []
@@ -222,11 +230,12 @@ def _get_channel_names(cursor, channel_ids):
row = cursor.execute('''SELECT channel_name
FROM subscribed_channels
WHERE yt_channel_id = ?''', (channel_id,)).fetchone()
- result.append( (channel_id, row[0]) )
+ result.append((channel_id, row[0]))
return result
-def _channels_with_tag(cursor, tag, order=False, exclude_muted=False, include_muted_status=False):
+def _channels_with_tag(cursor, tag, order=False, exclude_muted=False,
+ include_muted_status=False):
''' returns list of (channel_id, channel_name) '''
statement = '''SELECT yt_channel_id, channel_name'''
@@ -247,12 +256,15 @@ def _channels_with_tag(cursor, tag, order=False, exclude_muted=False, include_mu
return cursor.execute(statement, [tag]).fetchall()
+
def _schedule_checking(cursor, channel_id, next_check_time):
cursor.execute('''UPDATE subscribed_channels SET next_check_time = ? WHERE yt_channel_id = ?''', [int(next_check_time), channel_id])
+
def _is_muted(cursor, channel_id):
return bool(cursor.execute('''SELECT muted FROM subscribed_channels WHERE yt_channel_id=?''', [channel_id]).fetchone()[0])
+
units = collections.OrderedDict([
('year', 31536000), # 365*24*3600
('month', 2592000), # 30*24*3600
@@ -262,6 +274,8 @@ units = collections.OrderedDict([
('minute', 60),
('second', 1),
])
+
+
def youtube_timestamp_to_posix(dumb_timestamp):
''' Given a dumbed down timestamp such as 1 year ago, 3 hours ago,
approximates the unix time (seconds since 1/1/1970) '''
@@ -275,6 +289,7 @@ def youtube_timestamp_to_posix(dumb_timestamp):
unit = unit[:-1] # remove s from end
return now - quantifier*units[unit]
+
def posix_to_dumbed_down(posix_time):
'''Inverse of youtube_timestamp_to_posix.'''
delta = int(time.time() - posix_time)
@@ -293,12 +308,14 @@ def posix_to_dumbed_down(posix_time):
else:
raise Exception()
+
def exact_timestamp(posix_time):
result = time.strftime('%I:%M %p %m/%d/%y', time.localtime(posix_time))
if result[0] == '0': # remove 0 infront of hour (like 01:00 PM)
return result[1:]
return result
+
try:
existing_thumbnails = set(os.path.splitext(name)[0] for name in os.listdir(thumbnails_directory))
except FileNotFoundError:
@@ -314,6 +331,7 @@ checking_channels = set()
# Just to use for printing channel checking status to console without opening database
channel_names = dict()
+
def check_channel_worker():
while True:
channel_id = check_channels_queue.get()
@@ -324,12 +342,12 @@ def check_channel_worker():
finally:
checking_channels.remove(channel_id)
-for i in range(0,5):
+
+for i in range(0, 5):
gevent.spawn(check_channel_worker)
# ----------------------------
-
# --- Auto checking system - Spaghetti code ---
def autocheck_dispatcher():
'''Scans the auto_check_list. Sleeps until the earliest job is due, then adds that channel to the checking queue above. Can be sent a new job through autocheck_job_application'''
@@ -356,7 +374,7 @@ def autocheck_dispatcher():
if time_until_earliest_job > 0: # it can become less than zero (in the past) when it's set to go off while the dispatcher is doing something else at that moment
try:
- new_job = autocheck_job_application.get(timeout = time_until_earliest_job) # sleep for time_until_earliest_job time, but allow to be interrupted by new jobs
+ new_job = autocheck_job_application.get(timeout=time_until_earliest_job) # sleep for time_until_earliest_job time, but allow to be interrupted by new jobs
except gevent.queue.Empty: # no new jobs
pass
else: # new job, add it to the list
@@ -369,7 +387,10 @@ def autocheck_dispatcher():
check_channels_queue.put(earliest_job['channel_id'])
del autocheck_jobs[earliest_job_index]
+
dispatcher_greenlet = None
+
+
def start_autocheck_system():
global autocheck_job_application
global autocheck_jobs
@@ -398,30 +419,34 @@ def start_autocheck_system():
autocheck_jobs.append({'channel_id': row[0], 'channel_name': row[1], 'next_check_time': next_check_time})
dispatcher_greenlet = gevent.spawn(autocheck_dispatcher)
+
def stop_autocheck_system():
if dispatcher_greenlet is not None:
dispatcher_greenlet.kill()
+
def autocheck_setting_changed(old_value, new_value):
if new_value:
start_autocheck_system()
else:
stop_autocheck_system()
-settings.add_setting_changed_hook('autocheck_subscriptions',
+
+settings.add_setting_changed_hook(
+ 'autocheck_subscriptions',
autocheck_setting_changed)
if settings.autocheck_subscriptions:
start_autocheck_system()
# ----------------------------
-
def check_channels_if_necessary(channel_ids):
for channel_id in channel_ids:
if channel_id not in checking_channels:
checking_channels.add(channel_id)
check_channels_queue.put(channel_id)
+
def _get_atoma_feed(channel_id):
url = 'https://www.youtube.com/feeds/videos.xml?channel_id=' + channel_id
try:
@@ -432,6 +457,7 @@ def _get_atoma_feed(channel_id):
return ''
raise
+
def _get_channel_tab(channel_id, channel_status_name):
try:
return channel.get_channel_tab(channel_id, print_status=False)
@@ -447,6 +473,7 @@ def _get_channel_tab(channel_id, channel_status_name):
return None
raise
+
def _get_upstream_videos(channel_id):
try:
channel_status_name = channel_names[channel_id]
@@ -527,9 +554,8 @@ def _get_upstream_videos(channel_id):
video_item['channel_id'] = channel_id
-
if len(videos) == 0:
- average_upload_period = 4*7*24*3600 # assume 1 month for channel with no videos
+ average_upload_period = 4*7*24*3600 # assume 1 month for channel with no videos
elif len(videos) < 5:
average_upload_period = int((time.time() - videos[len(videos)-1]['time_published'])/len(videos))
else:
@@ -591,7 +617,6 @@ def _get_upstream_videos(channel_id):
video_item['description'],
))
-
cursor.executemany('''INSERT OR IGNORE INTO videos (
sql_channel_id,
video_id,
@@ -619,7 +644,6 @@ def _get_upstream_videos(channel_id):
print(str(number_of_new_videos) + ' new videos from ' + channel_status_name)
-
def check_all_channels():
with open_database() as connection:
with connection as cursor:
@@ -654,22 +678,20 @@ def check_specific_channels(channel_ids):
check_channels_if_necessary(channel_ids)
-
@yt_app.route('/import_subscriptions', methods=['POST'])
def import_subscriptions():
# check if the post request has the file part
if 'subscriptions_file' not in request.files:
- #flash('No file part')
+ # flash('No file part')
return flask.redirect(util.URL_ORIGIN + request.full_path)
file = request.files['subscriptions_file']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
- #flash('No selected file')
+ # flash('No selected file')
return flask.redirect(util.URL_ORIGIN + request.full_path)
-
mime_type = file.mimetype
if mime_type == 'application/json':
@@ -681,7 +703,7 @@ def import_subscriptions():
return '400 Bad Request: Invalid json file', 400
try:
- channels = ( (item['snippet']['resourceId']['channelId'], item['snippet']['title']) for item in file)
+ channels = ((item['snippet']['resourceId']['channelId'], item['snippet']['title']) for item in file)
except (KeyError, IndexError):
traceback.print_exc()
return '400 Bad Request: Unknown json structure', 400
@@ -695,11 +717,10 @@ def import_subscriptions():
if (outline_element.tag != 'outline') or ('xmlUrl' not in outline_element.attrib):
continue
-
channel_name = outline_element.attrib['text']
channel_rss_url = outline_element.attrib['xmlUrl']
channel_id = channel_rss_url[channel_rss_url.find('channel_id=')+11:].strip()
- channels.append( (channel_id, channel_name) )
+ channels.append((channel_id, channel_name))
except (AssertionError, IndexError, defusedxml.ElementTree.ParseError) as e:
return '400 Bad Request: Unable to read opml xml file, or the file is not the expected format', 400
@@ -711,7 +732,6 @@ def import_subscriptions():
return flask.redirect(util.URL_ORIGIN + '/subscription_manager', 303)
-
@yt_app.route('/subscription_manager', methods=['GET'])
def get_subscription_manager_page():
group_by_tags = request.args.get('group_by_tags', '0') == '1'
@@ -731,7 +751,7 @@ def get_subscription_manager_page():
'tags': [t for t in _get_tags(cursor, channel_id) if t != tag],
})
- tag_groups.append( (tag, sub_list) )
+ tag_groups.append((tag, sub_list))
# Channels with no tags
channel_list = cursor.execute('''SELECT yt_channel_id, channel_name, muted
@@ -751,7 +771,7 @@ def get_subscription_manager_page():
'tags': [],
})
- tag_groups.append( ('No tags', sub_list) )
+ tag_groups.append(('No tags', sub_list))
else:
sub_list = []
for channel_name, channel_id, muted in _get_subscribed_channels(cursor):
@@ -763,20 +783,20 @@ def get_subscription_manager_page():
'tags': _get_tags(cursor, channel_id),
})
-
-
-
if group_by_tags:
- return flask.render_template('subscription_manager.html',
- group_by_tags = True,
- tag_groups = tag_groups,
+ return flask.render_template(
+ 'subscription_manager.html',
+ group_by_tags=True,
+ tag_groups=tag_groups,
)
else:
- return flask.render_template('subscription_manager.html',
- group_by_tags = False,
- sub_list = sub_list,
+ return flask.render_template(
+ 'subscription_manager.html',
+ group_by_tags=False,
+ sub_list=sub_list,
)
+
def list_from_comma_separated_tags(string):
return [tag.strip() for tag in string.split(',') if tag.strip()]
@@ -795,7 +815,7 @@ def post_subscription_manager_page():
_unsubscribe(cursor, request.values.getlist('channel_ids'))
elif action == 'unsubscribe_verify':
unsubscribe_list = _get_channel_names(cursor, request.values.getlist('channel_ids'))
- return flask.render_template('unsubscribe_verify.html', unsubscribe_list = unsubscribe_list)
+ return flask.render_template('unsubscribe_verify.html', unsubscribe_list=unsubscribe_list)
elif action == 'mute':
cursor.executemany('''UPDATE subscribed_channels
@@ -810,6 +830,7 @@ def post_subscription_manager_page():
return flask.redirect(util.URL_ORIGIN + request.full_path, 303)
+
@yt_app.route('/subscriptions', methods=['GET'])
@yt_app.route('/feed/subscriptions', methods=['GET'])
def get_subscriptions_page():
@@ -826,7 +847,6 @@ def get_subscriptions_page():
tags = _get_all_tags(cursor)
-
subscription_list = []
for channel_name, channel_id, muted in _get_subscribed_channels(cursor):
subscription_list.append({
@@ -836,16 +856,18 @@ def get_subscriptions_page():
'muted': muted,
})
- return flask.render_template('subscriptions.html',
- header_playlist_names = local_playlist.get_playlist_names(),
- videos = videos,
- num_pages = math.ceil(number_of_videos_in_db/60),
- parameters_dictionary = request.args,
- tags = tags,
- current_tag = tag,
- subscription_list = subscription_list,
+ return flask.render_template(
+ 'subscriptions.html',
+ header_playlist_names=local_playlist.get_playlist_names(),
+ videos=videos,
+ num_pages=math.ceil(number_of_videos_in_db/60),
+ parameters_dictionary=request.args,
+ tags=tags,
+ current_tag=tag,
+ subscription_list=subscription_list,
)
+
@yt_app.route('/subscriptions', methods=['POST'])
@yt_app.route('/feed/subscriptions', methods=['POST'])
def post_subscriptions_page():
@@ -900,17 +922,10 @@ def serve_subscription_thumbnail(thumbnail):
try:
f = open(thumbnail_path, 'wb')
except FileNotFoundError:
- os.makedirs(thumbnails_directory, exist_ok = True)
+ os.makedirs(thumbnails_directory, exist_ok=True)
f = open(thumbnail_path, 'wb')
f.write(image)
f.close()
existing_thumbnails.add(video_id)
return flask.Response(image, mimetype='image/jpeg')
-
-
-
-
-
-
-