diff --git a/frontends/ycdl_flask/ycdl_flask/ycdl_flask.py b/frontends/ycdl_flask/ycdl_flask/ycdl_flask.py index e77697c..4b05c08 100644 --- a/frontends/ycdl_flask/ycdl_flask/ycdl_flask.py +++ b/frontends/ycdl_flask/ycdl_flask/ycdl_flask.py @@ -29,7 +29,7 @@ STATIC_DIR = root_dir.with_child('static') FAVICON_PATH = STATIC_DIR.with_child('favicon.png') youtube_core = ycdl.ytapi.Youtube(bot.get_youtube_key()) -youtube = ycdl.YCDL(youtube_core) +ycdldb = ycdl.ycdldb.YCDLDB(youtube_core) site = flask.Flask( __name__, @@ -136,9 +136,9 @@ def favicon(): @site.route('/channels') def get_channels(): - channels = youtube.get_channels() + channels = ycdldb.get_channels() for channel in channels: - channel['has_pending'] = youtube.channel_has_pending(channel['id']) + channel['has_pending'] = ycdldb.channel_has_pending(channel['id']) return flask.render_template('channels.html', channels=channels) @site.route('/videos') @@ -149,14 +149,14 @@ def get_channels(): def get_channel(channel_id=None, download_filter=None): if channel_id is not None: try: - youtube.add_channel(channel_id) + ycdldb.add_channel(channel_id) except Exception: traceback.print_exc() - channel = youtube.get_channel(channel_id) + channel = ycdldb.get_channel(channel_id) else: channel = None - videos = youtube.get_videos(channel_id=channel_id, download_filter=download_filter) + videos = ycdldb.get_videos(channel_id=channel_id, download_filter=download_filter) search_terms = request.args.get('q', '').lower().strip().replace('+', ' ').split() if search_terms: @@ -164,8 +164,8 @@ def get_channel(channel_id=None, download_filter=None): video_id = request.args.get('v', '') if video_id: - youtube.insert_video(video_id) - videos = [youtube.get_video(video_id)] + ycdldb.insert_video(video_id) + videos = [ycdldb.get_video(video_id)] limit = request.args.get('limit', None) if limit is not None: @@ -184,8 +184,8 @@ def get_channel(channel_id=None, download_filter=None): 'channel.html', channel=channel, download_filter=download_filter, - videos=videos, query_string='?' + request.query_string.decode('utf-8'), + videos=videos, ) @site.route('/mark_video_state', methods=['POST']) @@ -197,16 +197,16 @@ def post_mark_video_state(): try: video_ids = video_ids.split(',') for video_id in video_ids: - youtube.mark_video_state(video_id, state, commit=False) - youtube.sql.commit() + ycdldb.mark_video_state(video_id, state, commit=False) + ycdldb.sql.commit() - except ycdl.NoSuchVideo: - youtube.rollback() + except ycdl.exceptions.NoSuchVideo: + ycdldb.rollback() traceback.print_exc() flask.abort(404) - except ycdl.InvalidVideoState: - youtube.rollback() + except ycdl.exceptions.InvalidVideoState: + ycdldb.rollback() flask.abort(400) return make_json_response({'video_ids': video_ids, 'state': state}) @@ -215,7 +215,7 @@ def post_mark_video_state(): def post_refresh_all_channels(): force = request.form.get('force', False) force = ycdl.helpers.truthystring(force) - youtube.refresh_all_channels(force=force) + ycdldb.refresh_all_channels(force=force) return make_json_response({}) @site.route('/refresh_channel', methods=['POST']) @@ -229,14 +229,14 @@ def post_refresh_channel(): if not (len(channel_id) == 24 and channel_id.startswith('UC')): # It seems they have given us a username instead. try: - channel_id = youtube.youtube.get_user_id(username=channel_id) + channel_id = ycdldb.youtube.get_user_id(username=channel_id) except IndexError: flask.abort(404) force = request.form.get('force', False) force = ycdl.helpers.truthystring(force) - youtube.add_channel(channel_id, commit=False) - youtube.refresh_channel(channel_id, force=force) + ycdldb.add_channel(channel_id, commit=False) + ycdldb.refresh_channel(channel_id, force=force) return make_json_response({}) @site.route('/start_download', methods=['POST']) @@ -247,11 +247,11 @@ def post_start_download(): try: video_ids = video_ids.split(',') for video_id in video_ids: - youtube.download_video(video_id, commit=False) - youtube.sql.commit() + ycdldb.download_video(video_id, commit=False) + ycdldb.sql.commit() except ycdl.ytapi.VideoNotFound: - youtube.rollback() + ycdldb.rollback() flask.abort(404) return make_json_response({'video_ids': video_ids, 'state': 'downloaded'}) @@ -266,7 +266,7 @@ def refresher_thread(): time.sleep(60 * 60 * 6) print('Starting refresh job.') thread_kwargs = {'force': False, 'skip_failures': True} - refresh_job = threading.Thread(target=youtube.refresh_all_channels, kwargs=thread_kwargs, daemon=True) + refresh_job = threading.Thread(target=ycdldb.refresh_all_channels, kwargs=thread_kwargs, daemon=True) refresh_job.start() refresher = threading.Thread(target=refresher_thread, daemon=True) diff --git a/ycdl/__init__.py b/ycdl/__init__.py index b120238..4bbe3b5 100644 --- a/ycdl/__init__.py +++ b/ycdl/__init__.py @@ -1,5 +1,4 @@ +from . import exceptions from . import helpers -from . import ycdl +from . import ycdldb from . import ytapi - -YCDL = ycdl.YCDL diff --git a/ycdl/exceptions.py b/ycdl/exceptions.py new file mode 100644 index 0000000..0c70199 --- /dev/null +++ b/ycdl/exceptions.py @@ -0,0 +1,53 @@ +import re + +def pascal_to_loudsnakes(text): + ''' + NoSuchPhoto -> NO_SUCH_PHOTO + ''' + match = re.findall(r'[A-Z][a-z]*', text) + text = '_'.join(match) + text = text.upper() + return text + +class ErrorTypeAdder(type): + ''' + During definition, the Exception class will automatically receive a class + attribute called `error_type` which is just the class's name as a string + in the loudsnake casing style. NoSuchPhoto -> NO_SUCH_PHOTO. + + This is used for serialization of the exception object and should + basically act as a status code when displaying the error to the user. + + Thanks Unutbu + http://stackoverflow.com/a/18126678 + ''' + def __init__(cls, name, bases, clsdict): + type.__init__(cls, name, bases, clsdict) + cls.error_type = pascal_to_loudsnakes(name) + +class YCDLException(Exception, metaclass=ErrorTypeAdder): + ''' + Subtypes should have a class attribute `error_message`. The error message + may contain {format} strings which will be formatted using the + Exception's constructor arguments. + ''' + error_message = '' + + def __init__(self, *args, **kwargs): + super().__init__() + self.given_args = args + self.given_kwargs = kwargs + self.error_message = self.error_message.format(*args, **kwargs) + self.args = (self.error_message, args, kwargs) + + def __str__(self): + return self.error_type + '\n' + self.error_message + +class InvalidVideoState(YCDLException): + error_message = '{} is not a valid state.' + +class NoSuchVideo(YCDLException): + error_message = 'Video {} does not exist.' + +class DatabaseOutOfDate(YCDLException): + error_message = 'Database is out-of-date. {current} should be {new}.' diff --git a/ycdl/ycdl.py b/ycdl/ycdldb.py similarity index 94% rename from ycdl/ycdl.py rename to ycdl/ycdldb.py index fdf4d38..fe61b63 100644 --- a/ycdl/ycdl.py +++ b/ycdl/ycdldb.py @@ -3,6 +3,7 @@ import os import sqlite3 import traceback +from . import exceptions from . import helpers from . import ytapi @@ -10,7 +11,7 @@ from voussoirkit import sqlhelpers def YOUTUBE_DL_COMMAND(video_id): - path = 'D:\\Incoming\\ytqueue\\{id}.ytqueue'.format(id=video_id) + path = f'D:\\Incoming\\ytqueue\\{video_id}.ytqueue' open(path, 'w') logging.basicConfig(level=logging.DEBUG) @@ -74,9 +75,6 @@ SQL_VIDEO = {key:index for (index, key) in enumerate(SQL_VIDEO_COLUMNS)} DEFAULT_DBNAME = 'ycdl.db' -ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}.' - - def assert_is_abspath(path): ''' TO DO: Determine whether this is actually correct. @@ -85,13 +83,7 @@ def assert_is_abspath(path): raise ValueError('Not an abspath') -class InvalidVideoState(Exception): - pass - -class NoSuchVideo(Exception): - pass - -class YCDL: +class YCDLDB: def __init__(self, youtube, database_filename=None, youtube_dl_function=None): self.youtube = youtube if database_filename is None: @@ -105,10 +97,7 @@ class YCDL: self.cur.execute('PRAGMA user_version') existing_version = self.cur.fetchone()[0] if existing_version != DATABASE_VERSION: - message = ERROR_DATABASE_OUTOFDATE - message = message.format(current=existing_version, new=DATABASE_VERSION) - print(message) - raise SystemExit + raise exceptions.DatabaseOutOfDate(current=existing_version, new=DATABASE_VERSION) if youtube_dl_function: self.youtube_dl_function = youtube_dl_function @@ -203,6 +192,15 @@ class YCDL: if commit: self.sql.commit() + def get_all_states(self): + query = 'SELECT DISTINCT download FROM videos' + self.cur.execute(query) + states = self.cur.fetchall() + if states is None: + return [] + states = [row[0] for row in states] + return sorted(states) + def get_channel(self, channel_id): self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) fetch = self.cur.fetchone() @@ -294,11 +292,11 @@ class YCDL: ''' Mark the video as ignored, pending, or downloaded. ''' - if state not in ['ignored', 'pending', 'downloaded']: - raise InvalidVideoState(state) + if state not in ['ignored', 'pending', 'downloaded', 'coldstorage']: + raise exceptions.InvalidVideoState(state) self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) if self.cur.fetchone() is None: - raise NoSuchVideo(video_id) + raise exceptions.NoSuchVideo(video_id) self.cur.execute('UPDATE videos SET download = ? WHERE id == ?', [state, video_id]) if commit: self.sql.commit()