diff options
-rw-r--r-- | youtube/subscriptions.py | 210 | ||||
-rw-r--r-- | youtube/templates/subscription_manager.html | 141 | ||||
-rw-r--r-- | youtube/templates/unsubscribe_verify.html | 19 | ||||
-rw-r--r-- | yt_subscription_manager_template.html | 120 |
4 files changed, 242 insertions, 248 deletions
diff --git a/youtube/subscriptions.py b/youtube/subscriptions.py index a238ae4..fa0570c 100644 --- a/youtube/subscriptions.py +++ b/youtube/subscriptions.py @@ -326,28 +326,38 @@ def check_specific_channels(channel_ids): +@yt_app.route('/import_subscriptions', methods=['POST']) +def import_subscriptions(): -def import_subscriptions(env, start_response): - content_type = env['parameters']['subscriptions_file'][0] - file = env['parameters']['subscriptions_file'][1] + # check if the post request has the file part + if 'subscriptions_file' not in request.files: + #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') + return flask.redirect(util.URL_ORIGIN + request.full_path) - file = file.decode('utf-8') - if content_type == 'application/json': + mime_type = file.mimetype + + if mime_type == 'application/json': + file = file.read().decode('utf-8') try: file = json.loads(file) except json.decoder.JSONDecodeError: traceback.print_exc() - start_response('400 Bad Request', () ) - return b'400 Bad Request: Invalid json file' + return '400 Bad Request: Invalid json file', 400 try: channels = ( (item['snippet']['resourceId']['channelId'], item['snippet']['title']) for item in file) except (KeyError, IndexError): traceback.print_exc() - start_response('400 Bad Request', () ) - return b'400 Bad Request: Unknown json structure' - elif content_type in ('application/xml', 'text/xml', 'text/x-opml'): + return '400 Bad Request: Unknown json structure', 400 + elif mime_type in ('application/xml', 'text/xml', 'text/x-opml'): + file = file.read().decode('utf-8') try: root = defusedxml.ElementTree.fromstring(file) assert root.tag == 'opml' @@ -363,57 +373,36 @@ def import_subscriptions(env, start_response): channels.append( (channel_id, channel_name) ) except (AssertionError, IndexError, defusedxml.ElementTree.ParseError) as e: - start_response('400 Bad Request', () ) - return b'400 Bad Request: Unable to read opml xml file, or the file is not the expected format' + return '400 Bad Request: Unable to read opml xml file, or the file is not the expected format', 400 else: - start_response('400 Bad Request', () ) - return b'400 Bad Request: Unsupported file format: ' + html.escape(content_type).encode('utf-8') + b'. Only subscription.json files (from Google Takeouts) and XML OPML files exported from Youtube\'s subscription manager page are supported' + return '400 Bad Request: Unsupported file format: ' + mime_type + '. Only subscription.json files (from Google Takeouts) and XML OPML files exported from Youtube\'s subscription manager page are supported', 400 with_open_db(_subscribe, channels) - start_response('303 See Other', [('Location', util.URL_ORIGIN + '/subscription_manager'),] ) - return b'' - + return flask.redirect(util.URL_ORIGIN + '/subscription_manager', 303) -"""sub_list_item_template = Template(''' -<li class="sub-list-item $mute_class"> - <input class="sub-list-checkbox" name="channel_ids" value="$channel_id" form="subscription-manager-form" type="checkbox"> - <a href="$channel_url" class="sub-list-item-name" title="$channel_name">$channel_name</a> - <span class="tag-list">$tags</span> -</li>''') -tag_group_template = Template(''' -<li class="tag-group"> - <h2 class="tag-group-name">$tag</h2> - <ol class="sub-list"> -$sub_list - </ol> -</li> -''')""" -def get_subscription_manager_page(env, start_response): +@yt_app.route('/subscription_manager', methods=['GET']) +def get_subscription_manager_page(): + group_by_tags = request.args.get('group_by_tags', '0') == '1' with open_database() as connection: with connection as cursor: - if env['parameters'].get('group_by_tags', '0')[0] == '1': + if group_by_tags: + tag_groups = [] - sort_name = "Don't group" - sort_link = util.URL_ORIGIN + '/subscription_manager' - - main_list_html = '<ul class="tag-group-list">' for tag in _get_all_tags(cursor): - sub_list_html = '' + sub_list = [] for channel_id, channel_name, muted in _channels_with_tag(cursor, tag, order=True, include_muted_status=True): - sub_list_html += sub_list_item_template.substitute( - channel_url = util.URL_ORIGIN + '/channel/' + channel_id, - channel_name = html.escape(channel_name), - channel_id = channel_id, - tags = ', '.join(t for t in _get_tags(cursor, channel_id) if t != tag), - mute_class = 'muted' if muted else '', - ) - main_list_html += tag_group_template.substitute( - tag = tag, - sub_list = sub_list_html, - ) + sub_list.append({ + 'channel_url': util.URL_ORIGIN + '/channel/' + channel_id, + 'channel_name': channel_name, + 'channel_id': channel_id, + 'muted': muted, + 'tags': [t for t in _get_tags(cursor, channel_id) if t != tag], + }) + + tag_groups.append( (tag, sub_list) ) # Channels with no tags channel_list = cursor.execute('''SELECT yt_channel_id, channel_name, muted @@ -423,109 +412,74 @@ def get_subscription_manager_page(env, start_response): ) ORDER BY channel_name COLLATE NOCASE''').fetchall() if channel_list: - sub_list_html = '' + sub_list = [] for channel_id, channel_name, muted in channel_list: - sub_list_html += sub_list_item_template.substitute( - channel_url = util.URL_ORIGIN + '/channel/' + channel_id, - channel_name = html.escape(channel_name), - channel_id = channel_id, - tags = '', - mute_class = 'muted' if muted else '', - ) - main_list_html += tag_group_template.substitute( - tag = "No tags", - sub_list = sub_list_html, - ) - main_list_html += '</ul>' - + sub_list.append({ + 'channel_url': util.URL_ORIGIN + '/channel/' + channel_id, + 'channel_name': channel_name, + 'channel_id': channel_id, + 'muted': muted, + 'tags': [], + }) + + tag_groups.append( ('No tags', sub_list) ) else: + sub_list = [] + for channel_name, channel_id, muted in _get_subscribed_channels(cursor): + sub_list.append({ + 'channel_url': util.URL_ORIGIN + '/channel/' + channel_id, + 'channel_name': channel_name, + 'channel_id': channel_id, + 'muted': muted, + 'tags': _get_tags(cursor, channel_id), + }) - sort_name = "Group by tags" - sort_link = util.URL_ORIGIN + '/subscription_manager?group_by_tags=1' - main_list_html = '<ol class="sub-list">' - for channel_name, channel_id, muted in _get_subscribed_channels(cursor): - main_list_html += sub_list_item_template.substitute( - channel_url = util.URL_ORIGIN + '/channel/' + channel_id, - channel_name = html.escape(channel_name), - channel_id = channel_id, - tags = ', '.join(_get_tags(cursor, channel_id)), - mute_class = 'muted' if muted else '', - ) - main_list_html += '</ol>' - - - - start_response('200 OK', [('Content-type','text/html'),]) - return subscription_manager_template.substitute( - header = html_common.get_header(), - main_list = main_list_html, - sort_name = sort_name, - sort_link = sort_link, - page_buttons = '', - ).encode('utf-8') + + + if group_by_tags: + 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, + ) def list_from_comma_separated_tags(string): return [tag.strip() for tag in string.split(',') if tag.strip()] -"""unsubscribe_list_item_template = Template(''' -<li><a href="$channel_url" title="$channel_name">$channel_name</a></li>''')""" -def post_subscription_manager_page(env, start_response): - params = env['parameters'] - action = params['action'][0] +@yt_app.route('/subscription_manager', methods=['POST']) +def post_subscription_manager_page(): + action = request.values['action'] with open_database() as connection: with connection as cursor: if action == 'add_tags': - _add_tags(cursor, params['channel_ids'], [tag.lower() for tag in list_from_comma_separated_tags(params['tags'][0])]) + _add_tags(cursor, request.values.getlist('channel_ids'), [tag.lower() for tag in list_from_comma_separated_tags(request.values['tags'])]) elif action == 'remove_tags': - _remove_tags(cursor, params['channel_ids'], [tag.lower() for tag in list_from_comma_separated_tags(params['tags'][0])]) + _remove_tags(cursor, request.values.getlist('channel_ids'), [tag.lower() for tag in list_from_comma_separated_tags(request.values['tags'])]) elif action == 'unsubscribe': - _unsubscribe(cursor, params['channel_ids']) + _unsubscribe(cursor, request.values.getlist('channel_ids')) elif action == 'unsubscribe_verify': - page = ''' - <span>Are you sure you want to unsubscribe from these channels?</span> - <form class="subscriptions-import-form" action="/youtube.com/subscription_manager" method="POST">''' - - for channel_id in params['channel_ids']: - page += '<input type="hidden" name="channel_ids" value="' + channel_id + '">\n' - - page += ''' - <input type="hidden" name="action" value="unsubscribe"> - <input type="submit" value="Yes, unsubscribe"> - </form> - <ul>''' - for channel_id, channel_name in _get_channel_names(cursor, params['channel_ids']): - page += unsubscribe_list_item_template.substitute( - channel_url = util.URL_ORIGIN + '/channel/' + channel_id, - channel_name = html.escape(channel_name), - ) - page += '''</ul>''' - - start_response('200 OK', [('Content-type','text/html'),]) - return html_common.yt_basic_template.substitute( - page_title = 'Unsubscribe?', - style = '', - header = html_common.get_header(), - page = page, - ).encode('utf-8') + unsubscribe_list = _get_channel_names(cursor, request.values.getlist('channel_ids')) + return flask.render_template('unsubscribe_verify.html', unsubscribe_list = unsubscribe_list) + elif action == 'mute': cursor.executemany('''UPDATE subscribed_channels SET muted = 1 - WHERE yt_channel_id = ?''', [(ci,) for ci in params['channel_ids']]) + WHERE yt_channel_id = ?''', [(ci,) for ci in request.values.getlist('channel_ids')]) elif action == 'unmute': cursor.executemany('''UPDATE subscribed_channels SET muted = 0 - WHERE yt_channel_id = ?''', [(ci,) for ci in params['channel_ids']]) - + WHERE yt_channel_id = ?''', [(ci,) for ci in request.values.getlist('channel_ids')]) else: - start_response('400 Bad Request', ()) - return b'400 Bad Request' - - start_response('303 See Other', [('Location', util.URL_ORIGIN + '/subscription_manager'),] ) - return b'' + flask.abort(400) + return flask.redirect(util.URL_ORIGIN + request.full_path, 303) @yt_app.route('/subscriptions', methods=['GET']) @yt_app.route('/feed/subscriptions', methods=['GET']) diff --git a/youtube/templates/subscription_manager.html b/youtube/templates/subscription_manager.html new file mode 100644 index 0000000..838f4af --- /dev/null +++ b/youtube/templates/subscription_manager.html @@ -0,0 +1,141 @@ +{% set page_title = 'Subscription Manager' %} +{% extends "base.html" %} +{% block style %} + .import-export{ + display: flex; + flex-direction: row; + } + .subscriptions-import-form{ + background-color: #dadada; + display: flex; + flex-direction: column; + align-items: flex-start; + max-width: 300px; + padding:10px; + } + .subscriptions-import-form h2{ + font-size: 20px; + margin-bottom: 10px; + } + + .import-submit-button{ + margin-top:15px; + align-self: flex-end; + } + + + .subscriptions-export-links{ + margin: 0px 0px 0px 20px; + background-color: #dadada; + list-style: none; + max-width: 300px; + padding:10px; + } + + .sub-list-controls{ + background-color: #dadada; + padding:10px; + } + + + .tag-group-list{ + list-style: none; + margin-left: 10px; + margin-right: 10px; + padding: 0px; + } + .tag-group{ + border-style: solid; + margin-bottom: 10px; + } + + .sub-list{ + list-style: none; + padding:10px; + column-width: 300px; + column-gap: 40px; + } + .sub-list-item{ + display:flex; + margin-bottom: 10px; + background-color: #dadada; + } + .tag-list{ + margin-left:15px; + font-weight:bold; + } + .sub-list-item-name{ + margin-left:15px; + } + .sub-list-checkbox{ + height: 1.5em; + width: 1.5em; + } + .muted{ + background-color: #888888; + } +{% endblock style %} + + +{% macro subscription_list(sub_list) %} + {% for subscription in sub_list %} + <li class="sub-list-item {{ 'muted' if subscription['muted'] else '' }}"> + <input class="sub-list-checkbox" name="channel_ids" value="{{ subscription['channel_id'] }}" form="subscription-manager-form" type="checkbox"> + <a href="{{ subscription['channel_url'] }}" class="sub-list-item-name" title="{{ subscription['channel_name'] }}">{{ subscription['channel_name'] }}</a> + <span class="tag-list">{{ ', '.join(subscription['tags']) }}</span> + </li> + {% endfor %} +{% endmacro %} + + + +{% block main %} + <div class="import-export"> + <form class="subscriptions-import-form" enctype="multipart/form-data" action="/youtube.com/import_subscriptions" method="POST"> + <h2>Import subscriptions</h2> + <input type="file" id="subscriptions-import" accept="application/json, application/xml, text/x-opml" name="subscriptions_file"> + <input type="submit" value="Import" class="import-submit-button"> + </form> + + <ul class="subscriptions-export-links"> + <li><a href="/youtube.com/subscriptions.opml">Export subscriptions (OPML)</a></li> + <li><a href="/youtube.com/subscriptions.xml">Export subscriptions (RSS)</a></li> + </ul> + </div> + + <hr> + + <form id="subscription-manager-form" class="sub-list-controls" method="POST"> + {% if group_by_tags %} + <a class="sort-button" href="/https://www.youtube.com/subscription_manager?group_by_tags=0">Don't group</a> + {% else %} + <a class="sort-button" href="/https://www.youtube.com/subscription_manager?group_by_tags=1">Group by tags</a> + {% endif %} + <input type="text" name="tags"> + <button type="submit" name="action" value="add_tags">Add tags</button> + <button type="submit" name="action" value="remove_tags">Remove tags</button> + <button type="submit" name="action" value="unsubscribe_verify">Unsubscribe</button> + <button type="submit" name="action" value="mute">Mute</button> + <button type="submit" name="action" value="unmute">Unmute</button> + <input type="reset" value="Clear Selection"> + </form> + + + {% if group_by_tags %} + <ul class="tag-group-list"> + {% for tag_name, sub_list in tag_groups %} + <li class="tag-group"> + <h2 class="tag-group-name">{{ tag_name }}</h2> + <ol class="sub-list"> + {{ subscription_list(sub_list) }} + </ol> + </li> + {% endfor %} + </ul> + {% else %} + <ol class="sub-list"> + {{ subscription_list(sub_list) }} + </ol> + {% endif %} + +{% endblock main %} diff --git a/youtube/templates/unsubscribe_verify.html b/youtube/templates/unsubscribe_verify.html new file mode 100644 index 0000000..98581c0 --- /dev/null +++ b/youtube/templates/unsubscribe_verify.html @@ -0,0 +1,19 @@ +{% set page_title = 'Unsubscribe?' %} +{% extends "base.html" %} + +{% block main %} + <span>Are you sure you want to unsubscribe from these channels?</span> + <form class="subscriptions-import-form" action="/youtube.com/subscription_manager" method="POST"> + {% for channel_id, channel_name in unsubscribe_list %} + <input type="hidden" name="channel_ids" value="{{ channel_id }}"> + {% endfor %} + + <input type="hidden" name="action" value="unsubscribe"> + <input type="submit" value="Yes, unsubscribe"> + </form> + <ul> + {% for channel_id, channel_name in unsubscribe_list %} + <li><a href="{{ '/https://www.youtube.com/channel/' + channel_id }}" title="{{ channel_name }}">{{ channel_name }}</a></li> + {% endfor %} + </ul> +{% endblock main %} diff --git a/yt_subscription_manager_template.html b/yt_subscription_manager_template.html deleted file mode 100644 index 8997861..0000000 --- a/yt_subscription_manager_template.html +++ /dev/null @@ -1,120 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <title>Subscription Manager</title> - <link href="/youtube.com/shared.css" type="text/css" rel="stylesheet"> - <link href="/youtube.com/favicon.ico" type="image/x-icon" rel="icon"> - <link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml"> - <style type="text/css"> - - .import-export{ - display: flex; - flex-direction: row; - } - .subscriptions-import-form{ - background-color: #dadada; - display: flex; - flex-direction: column; - align-items: flex-start; - max-width: 300px; - padding:10px; - } - .subscriptions-import-form h2{ - font-size: 20px; - margin-bottom: 10px; - } - - .import-submit-button{ - margin-top:15px; - align-self: flex-end; - } - - - .subscriptions-export-links{ - margin: 0px 0px 0px 20px; - background-color: #dadada; - list-style: none; - max-width: 300px; - padding:10px; - } - - .sub-list-controls{ - background-color: #dadada; - padding:10px; - } - - - .tag-group-list{ - list-style: none; - margin-left: 10px; - margin-right: 10px; - padding: 0px; - } - .tag-group{ - border-style: solid; - margin-bottom: 10px; - } - - .sub-list{ - list-style: none; - padding:10px; - column-width: 300px; - column-gap: 40px; - } - .sub-list-item{ - display:flex; - margin-bottom: 10px; - background-color: #dadada; - } - .tag-list{ - margin-left:15px; - font-weight:bold; - } - .sub-list-item-name{ - margin-left:15px; - } - .sub-list-checkbox{ - height: 1.5em; - width: 1.5em; - } - .muted{ - background-color: #888888; - } - </style> - </head> - <body> -$header - <main> - - <div class="import-export"> - <form class="subscriptions-import-form" enctype="multipart/form-data" action="/youtube.com/import_subscriptions" method="POST"> - <h2>Import subscriptions</h2> - <input type="file" id="subscriptions-import" accept="application/json, application/xml, text/x-opml" name="subscriptions_file"> - <input type="submit" value="Import" class="import-submit-button"> - </form> - - <ul class="subscriptions-export-links"> - <li><a href="/youtube.com/subscriptions.opml">Export subscriptions (OPML)</a></li> - <li><a href="/youtube.com/subscriptions.xml">Export subscriptions (RSS)</a></li> - </ul> - </div> - - <hr> - - <form id="subscription-manager-form" class="sub-list-controls" method="POST"> - <a class="sort-button" href="$sort_link">$sort_name</a> - <input type="text" name="tags"> - <button type="submit" name="action" value="add_tags">Add tags</button> - <button type="submit" name="action" value="remove_tags">Remove tags</button> - <button type="submit" name="action" value="unsubscribe_verify">Unsubscribe</button> - <button type="submit" name="action" value="mute">Mute</button> - <button type="submit" name="action" value="unmute">Unmute</button> - <input type="reset" value="Clear Selection"> - </form> - -$main_list - - </main> - </body> -</html> |