Changes
- Add thumbnails to the card (dynamically loaded) - Add button for embedding the video - Store video duration in db (use update script) - Separate links for filter views with and without query.
This commit is contained in:
		
							parent
							
								
									ba1961349c
								
							
						
					
					
						commit
						f1f12423b1
					
				
					 10 changed files with 483 additions and 332 deletions
				
			
		|  | @ -77,15 +77,15 @@ | ||||||
|     onclick="refresh_channel('{{channel['id']}}', false, function(){location.reload()})">Refresh new videos</button></span> |     onclick="refresh_channel('{{channel['id']}}', false, function(){location.reload()})">Refresh new videos</button></span> | ||||||
|     <span><button class="refresh_button" |     <span><button class="refresh_button" | ||||||
|     onclick="refresh_channel('{{channel['id']}}', true, function(){location.reload()})">Refresh everything</button></span> |     onclick="refresh_channel('{{channel['id']}}', true, function(){location.reload()})">Refresh everything</button></span> | ||||||
|     <span><a href="/channel/{{channel['id']}}{{query_string}}">All</a></span> |     <span><a href="/channel/{{channel['id']}}">All</a> <a href="/channel/{{channel['id']}}{{query_string}}">(?q)</a></span> | ||||||
|     <span><a href="/channel/{{channel['id']}}/pending{{query_string}}">Pending</a></span> |     <span><a href="/channel/{{channel['id']}}/pending">Pending</a> <a href="/channel/{{channel['id']}}/pending{{query_string}}">(?q)</a></span> | ||||||
|     <span><a href="/channel/{{channel['id']}}/ignored{{query_string}}">Ignored</a></span> |     <span><a href="/channel/{{channel['id']}}/ignored">Ignored</a> <a href="/channel/{{channel['id']}}/ignored{{query_string}}">(?q)</a></span> | ||||||
|     <span><a href="/channel/{{channel['id']}}/downloaded{{query_string}}">Downloaded</a></span> |     <span><a href="/channel/{{channel['id']}}/downloaded">Downloaded</a> <a href="/channel/{{channel['id']}}/downloaded{{query_string}}">(?q)</a></span> | ||||||
|     {% else %} |     {% else %} | ||||||
|     <span><a href="/videos{{query_string}}">All</a></span> |     <span><a href="/videos">All</a> <a href="/videos{{query_string}}">(?q)</a></span> | ||||||
|     <span><a href="/videos/pending{{query_string}}">Pending</a></span> |     <span><a href="/videos/pending">Pending</a> <a href="/videos/pending{{query_string}}">(?q)</a></span> | ||||||
|     <span><a href="/videos/ignored{{query_string}}">Ignored</a></span> |     <span><a href="/videos/ignored">Ignored</a> <a href="/videos/ignored{{query_string}}">(?q)</a></span> | ||||||
|     <span><a href="/videos/downloaded{{query_string}}">Downloaded</a></span> |     <span><a href="/videos/downloaded">Downloaded</a> <a href="/videos/downloaded{{query_string}}">(?q)</a></span> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     <span>{{videos|length}} items</span> |     <span>{{videos|length}} items</span> | ||||||
|      |      | ||||||
|  | @ -94,15 +94,11 @@ | ||||||
|         <div id="video_card_{{video['id']}}" |         <div id="video_card_{{video['id']}}" | ||||||
|         data-ytid="{{video['id']}}" |         data-ytid="{{video['id']}}" | ||||||
|         onclick="onclick_select(event)" |         onclick="onclick_select(event)" | ||||||
|         {% if video['download'] == "downloaded" %} |         class="video_card video_card_{{video['download']}}" | ||||||
|         class="video_card video_card_downloaded" |  | ||||||
|         {% elif video['download'] == "ignored" %} |  | ||||||
|         class="video_card video_card_ignored" |  | ||||||
|         {% else %} |  | ||||||
|         class="video_card video_card_pending" |  | ||||||
|         {% endif %} |  | ||||||
|         > |         > | ||||||
|  |             <img src="http://i3.ytimg.com/vi/{{video['id']}}/default.jpg" height="100px"> | ||||||
|             <a href="https://www.youtube.com/watch?v={{video['id']}}">{{video['_published_str']}} - {{video['title']}}</a> |             <a href="https://www.youtube.com/watch?v={{video['id']}}">{{video['_published_str']}} - {{video['title']}}</a> | ||||||
|  |             <span>({{video['duration'] | seconds_to_hms}})</span> | ||||||
|             {% if channel is none %} |             {% if channel is none %} | ||||||
|             <a href="/channel/{{video['author_id']}}">(Chan)</a> |             <a href="/channel/{{video['author_id']}}">(Chan)</a> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|  | @ -134,6 +130,10 @@ | ||||||
|                 onclick="action_button_passthrough(event, mark_video_state, 'ignored')" |                 onclick="action_button_passthrough(event, mark_video_state, 'ignored')" | ||||||
|                 >Ignore</button> |                 >Ignore</button> | ||||||
|             </div> |             </div> | ||||||
|  |             <br> | ||||||
|  |             <button class="show_embed_button" onclick="toggle_embed_video('{{video.id}}');">Embed</button> | ||||||
|  |             <button class="hide_embed_button hidden" onclick="toggle_embed_video('{{video.id}}');">Unembed</button> | ||||||
|  |             <br> | ||||||
|         </div> |         </div> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|     </div> |     </div> | ||||||
|  | @ -147,6 +147,28 @@ var video_card_first_selected = null; | ||||||
| var video_card_selections = []; | var video_card_selections = []; | ||||||
| var video_cards = Array.from(document.getElementById("video_cards").children); | var video_cards = Array.from(document.getElementById("video_cards").children); | ||||||
| 
 | 
 | ||||||
|  | function toggle_embed_video(video_id) | ||||||
|  | { | ||||||
|  |     var video_card = document.getElementById("video_card_" + video_id); | ||||||
|  |     var show_button = video_card.getElementsByClassName("show_embed_button")[0]; | ||||||
|  |     var hide_button = video_card.getElementsByClassName("hide_embed_button")[0]; | ||||||
|  |     var embeds = video_card.getElementsByTagName("iframe"); | ||||||
|  |     if (embeds.length == 0) | ||||||
|  |     { | ||||||
|  |         var html = `<iframe width="711" height="400" src="https://www.youtube.com/embed/${video_id}" frameborder="0" allow="encrypted-media" allowfullscreen></iframe>` | ||||||
|  |         var embed = html_to_element(html); | ||||||
|  |         video_card.appendChild(embed); | ||||||
|  |         show_button.classList.add("hidden"); | ||||||
|  |         hide_button.classList.remove("hidden"); | ||||||
|  |     } | ||||||
|  |     else | ||||||
|  |     { | ||||||
|  |         video_card.removeChild(embeds[0]); | ||||||
|  |         show_button.classList.remove("hidden"); | ||||||
|  |         hide_button.classList.add("hidden"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function deselect_all() | function deselect_all() | ||||||
| { | { | ||||||
|     var video_card_first_selected = null; |     var video_card_first_selected = null; | ||||||
|  | @ -157,6 +179,13 @@ function deselect_all() | ||||||
|     video_card_selections = []; |     video_card_selections = []; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function html_to_element(html) | ||||||
|  | { | ||||||
|  |     var template = document.createElement("template"); | ||||||
|  |     template.innerHTML = html; | ||||||
|  |     return template.content.firstChild; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function onclick_select(event) | function onclick_select(event) | ||||||
| { | { | ||||||
|     if (!event.target.classList.contains("video_card")) |     if (!event.target.classList.contains("video_card")) | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								frontends/ycdl_flask/ycdl_flask/jinja_filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontends/ycdl_flask/ycdl_flask/jinja_filters.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | import math | ||||||
|  | 
 | ||||||
|  | def seconds_to_hms(seconds): | ||||||
|  |     ''' | ||||||
|  |     Convert integer number of seconds to an hh:mm:ss string. | ||||||
|  |     Only the necessary fields are used. | ||||||
|  |     ''' | ||||||
|  |     if seconds is None: | ||||||
|  |         return '???' | ||||||
|  | 
 | ||||||
|  |     seconds = math.ceil(seconds) | ||||||
|  |     (minutes, seconds) = divmod(seconds, 60) | ||||||
|  |     (hours, minutes) = divmod(minutes, 60) | ||||||
|  |     parts = [] | ||||||
|  |     if hours: | ||||||
|  |         parts.append(hours) | ||||||
|  |     if minutes: | ||||||
|  |         parts.append(minutes) | ||||||
|  |     parts.append(seconds) | ||||||
|  |     hms = ':'.join(f'{part:02d}' for part in parts) | ||||||
|  |     return hms | ||||||
|  | @ -18,6 +18,8 @@ import ycdl | ||||||
| 
 | 
 | ||||||
| from voussoirkit import pathclass | from voussoirkit import pathclass | ||||||
| 
 | 
 | ||||||
|  | from . import jinja_filters | ||||||
|  | 
 | ||||||
| root_dir = pathclass.Path(__file__).parent.parent | root_dir = pathclass.Path(__file__).parent.parent | ||||||
| 
 | 
 | ||||||
| TEMPLATE_DIR = root_dir.with_child('templates') | TEMPLATE_DIR = root_dir.with_child('templates') | ||||||
|  | @ -37,6 +39,7 @@ site.config.update( | ||||||
|     TEMPLATES_AUTO_RELOAD=True, |     TEMPLATES_AUTO_RELOAD=True, | ||||||
| ) | ) | ||||||
| site.jinja_env.add_extension('jinja2.ext.do') | site.jinja_env.add_extension('jinja2.ext.do') | ||||||
|  | site.jinja_env.filters['seconds_to_hms'] = jinja_filters.seconds_to_hms | ||||||
| site.debug = True | site.debug = True | ||||||
| 
 | 
 | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
|  | @ -192,6 +195,7 @@ def post_mark_video_state(): | ||||||
|         youtube.mark_video_state(video_id, state) |         youtube.mark_video_state(video_id, state) | ||||||
| 
 | 
 | ||||||
|     except ycdl.NoSuchVideo: |     except ycdl.NoSuchVideo: | ||||||
|  |         traceback.print_exc() | ||||||
|         flask.abort(404) |         flask.abort(404) | ||||||
| 
 | 
 | ||||||
|     except ycdl.InvalidVideoState: |     except ycdl.InvalidVideoState: | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import gevent.monkey | ||||||
| gevent.monkey.patch_all() | gevent.monkey.patch_all() | ||||||
| 
 | 
 | ||||||
| import gevent.pywsgi | import gevent.pywsgi | ||||||
| import gevent.wsgi |  | ||||||
| import sys | import sys | ||||||
| 
 | 
 | ||||||
| import ycdl_flask | import ycdl_flask | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
| flask | flask | ||||||
| gevent | gevent | ||||||
| https://github.com/voussoir/else/raw/master/_voussoirkit/voussoirkit.zip | https://github.com/voussoir/else/raw/master/_voussoirkit/voussoirkit.zip | ||||||
|  | isodate | ||||||
|  |  | ||||||
							
								
								
									
										80
									
								
								utilities/database_upgrader.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								utilities/database_upgrader.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | ||||||
|  | import argparse | ||||||
|  | import os | ||||||
|  | import sqlite3 | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | import ycdl | ||||||
|  | 
 | ||||||
|  | def upgrade_1_to_2(sql): | ||||||
|  |     ''' | ||||||
|  |     In this version, a column `tagged_at` was added to the Photos table, to keep | ||||||
|  |     track of the last time the photo's tags were edited (added or removed). | ||||||
|  |     ''' | ||||||
|  |     cur = sql.cursor() | ||||||
|  |     cur.executescript(''' | ||||||
|  |         ALTER TABLE videos RENAME TO videos_old; | ||||||
|  |         CREATE TABLE videos( | ||||||
|  |             id TEXT, | ||||||
|  |             published INT, | ||||||
|  |             author_id TEXT, | ||||||
|  |             title TEXT, | ||||||
|  |             description TEXT, | ||||||
|  |             duration INT, | ||||||
|  |             thumbnail TEXT, | ||||||
|  |             download TEXT | ||||||
|  |         ); | ||||||
|  |         INSERT INTO videos SELECT | ||||||
|  |             id, | ||||||
|  |             published, | ||||||
|  |             author_id, | ||||||
|  |             title, | ||||||
|  |             description, | ||||||
|  |             NULL, | ||||||
|  |             thumbnail, | ||||||
|  |             download | ||||||
|  |         FROM videos_old; | ||||||
|  |         DROP TABLE videos_old; | ||||||
|  |     ''') | ||||||
|  | 
 | ||||||
|  | def upgrade_all(database_filepath): | ||||||
|  |     ''' | ||||||
|  |     Given the directory containing a phototagger database, apply all of the | ||||||
|  |     needed upgrade_x_to_y functions in order. | ||||||
|  |     ''' | ||||||
|  |     sql = sqlite3.connect(database_filepath) | ||||||
|  | 
 | ||||||
|  |     cur = sql.cursor() | ||||||
|  | 
 | ||||||
|  |     cur.execute('PRAGMA user_version') | ||||||
|  |     current_version = cur.fetchone()[0] | ||||||
|  |     needed_version = ycdl.ycdl.DATABASE_VERSION | ||||||
|  | 
 | ||||||
|  |     if current_version == needed_version: | ||||||
|  |         print('Already up to date with version %d.' % needed_version) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     for version_number in range(current_version + 1, needed_version + 1): | ||||||
|  |         print('Upgrading from %d to %d.' % (current_version, version_number)) | ||||||
|  |         upgrade_function = 'upgrade_%d_to_%d' % (current_version, version_number) | ||||||
|  |         upgrade_function = eval(upgrade_function) | ||||||
|  |         upgrade_function(sql) | ||||||
|  |         sql.cursor().execute('PRAGMA user_version = %d' % version_number) | ||||||
|  |         sql.commit() | ||||||
|  |         current_version = version_number | ||||||
|  |     print('Upgrades finished.') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def upgrade_all_argparse(args): | ||||||
|  |     return upgrade_all(database_filepath=args.database_filepath) | ||||||
|  | 
 | ||||||
|  | def main(argv): | ||||||
|  |     parser = argparse.ArgumentParser() | ||||||
|  | 
 | ||||||
|  |     parser.add_argument('database_filepath') | ||||||
|  |     parser.set_defaults(func=upgrade_all_argparse) | ||||||
|  | 
 | ||||||
|  |     args = parser.parse_args(argv) | ||||||
|  |     args.func(args) | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     raise SystemExit(main(sys.argv[1:])) | ||||||
|  | @ -1,12 +1,15 @@ | ||||||
| import bot | import logging | ||||||
|  | logging.basicConfig(level=logging.WARNING) | ||||||
|  | 
 | ||||||
|  | import bot3 as bot | ||||||
| import os | import os | ||||||
| import traceback | import traceback | ||||||
| import ycdl | import ycdl | ||||||
| import ytapi |  | ||||||
| import ycdl_repl | import ycdl_repl | ||||||
| from voussoirkit import downloady | from voussoirkit import downloady | ||||||
| 
 | 
 | ||||||
| youtube_core = ytapi.Youtube(bot.YOUTUBE_KEY) | 
 | ||||||
|  | youtube_core = ycdl.ytapi.Youtube(bot.YOUTUBE_KEY) | ||||||
| youtube = ycdl.YCDL(youtube_core) | youtube = ycdl.YCDL(youtube_core) | ||||||
| 
 | 
 | ||||||
| DIRECTORY = '.\\youtube thumbnails' | DIRECTORY = '.\\youtube thumbnails' | ||||||
|  |  | ||||||
							
								
								
									
										295
									
								
								ycdl/__init__.py
									
									
									
									
									
								
							
							
						
						
									
										295
									
								
								ycdl/__init__.py
									
									
									
									
									
								
							|  | @ -1,296 +1,5 @@ | ||||||
| import logging |  | ||||||
| import os |  | ||||||
| import sqlite3 |  | ||||||
| 
 |  | ||||||
| from . import helpers | from . import helpers | ||||||
|  | from . import ycdl | ||||||
| from . import ytapi | from . import ytapi | ||||||
| 
 | 
 | ||||||
| def YOUTUBE_DL_COMMAND(video_id): | YCDL = ycdl.YCDL | ||||||
|     path = 'D:\\Incoming\\ytqueue\\{id}.ytqueue'.format(id=video_id) |  | ||||||
|     open(path, 'w') |  | ||||||
| 
 |  | ||||||
| logging.basicConfig(level=logging.DEBUG) |  | ||||||
| log = logging.getLogger(__name__) |  | ||||||
| logging.getLogger('googleapiclient.discovery').setLevel(logging.WARNING) |  | ||||||
| logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARNING) |  | ||||||
| logging.getLogger('requests.packages.urllib3.util.retry').setLevel(logging.WARNING) |  | ||||||
| 
 |  | ||||||
| SQL_CHANNEL_COLUMNS = [ |  | ||||||
|     'id', |  | ||||||
|     'name', |  | ||||||
|     'directory', |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| SQL_VIDEO_COLUMNS = [ |  | ||||||
|     'id', |  | ||||||
|     'published', |  | ||||||
|     'author_id', |  | ||||||
|     'title', |  | ||||||
|     'description', |  | ||||||
|     'thumbnail', |  | ||||||
|     'download', |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| SQL_CHANNEL = {key:index for (index, key) in enumerate(SQL_CHANNEL_COLUMNS)} |  | ||||||
| SQL_VIDEO = {key:index for (index, key) in enumerate(SQL_VIDEO_COLUMNS)} |  | ||||||
| 
 |  | ||||||
| DATABASE_VERSION = 1 |  | ||||||
| DB_INIT = ''' |  | ||||||
| PRAGMA count_changes = OFF; |  | ||||||
| PRAGMA cache_size = 10000; |  | ||||||
| PRAGMA user_version = {user_version}; |  | ||||||
| CREATE TABLE IF NOT EXISTS channels( |  | ||||||
|     id TEXT, |  | ||||||
|     name TEXT, |  | ||||||
|     directory TEXT COLLATE NOCASE |  | ||||||
| ); |  | ||||||
| CREATE TABLE IF NOT EXISTS videos( |  | ||||||
|     id TEXT, |  | ||||||
|     published INT, |  | ||||||
|     author_id TEXT, |  | ||||||
|     title TEXT, |  | ||||||
|     description TEXT, |  | ||||||
|     thumbnail TEXT, |  | ||||||
|     download TEXT |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| CREATE INDEX IF NOT EXISTS index_channel_id on channels(id); |  | ||||||
| CREATE INDEX IF NOT EXISTS index_video_id on videos(id); |  | ||||||
| CREATE INDEX IF NOT EXISTS index_video_published on videos(published); |  | ||||||
| CREATE INDEX IF NOT EXISTS index_video_download on videos(download); |  | ||||||
| '''.format(user_version=DATABASE_VERSION) |  | ||||||
| 
 |  | ||||||
| DEFAULT_DBNAME = 'ycdl.db' |  | ||||||
| 
 |  | ||||||
| ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}' |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def verify_is_abspath(path): |  | ||||||
|     ''' |  | ||||||
|     TO DO: Determine whether this is actually correct. |  | ||||||
|     ''' |  | ||||||
|     if os.path.abspath(path) != path: |  | ||||||
|         raise ValueError('Not an abspath') |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class InvalidVideoState(Exception): |  | ||||||
|     pass |  | ||||||
| 
 |  | ||||||
| class NoSuchVideo(Exception): |  | ||||||
|     pass |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class YCDL: |  | ||||||
|     def __init__(self, youtube, database_filename=None, youtube_dl_function=None): |  | ||||||
|         self.youtube = youtube |  | ||||||
|         if database_filename is None: |  | ||||||
|             database_filename = DEFAULT_DBNAME |  | ||||||
| 
 |  | ||||||
|         existing_database = os.path.exists(database_filename) |  | ||||||
|         self.sql = sqlite3.connect(database_filename) |  | ||||||
|         self.cur = self.sql.cursor() |  | ||||||
| 
 |  | ||||||
|         if existing_database: |  | ||||||
|             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 |  | ||||||
| 
 |  | ||||||
|         if youtube_dl_function: |  | ||||||
|             self.youtube_dl_function = youtube_dl_function |  | ||||||
|         else: |  | ||||||
|             self.youtube_dl_function = YOUTUBE_DL_COMMAND |  | ||||||
| 
 |  | ||||||
|         statements = DB_INIT.split(';') |  | ||||||
|         for statement in statements: |  | ||||||
|             self.cur.execute(statement) |  | ||||||
|         self.sql.commit() |  | ||||||
| 
 |  | ||||||
|     def add_channel( |  | ||||||
|             self, |  | ||||||
|             channel_id, |  | ||||||
|             commit=True, |  | ||||||
|             download_directory=None, |  | ||||||
|             get_videos=False, |  | ||||||
|             name=None, |  | ||||||
|         ): |  | ||||||
|         if self.get_channel(channel_id) is not None: |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         if name is None: |  | ||||||
|             name = self.youtube.get_user_name(channel_id) |  | ||||||
| 
 |  | ||||||
|         data = [None] * len(SQL_CHANNEL) |  | ||||||
|         data[SQL_CHANNEL['id']] = channel_id |  | ||||||
|         data[SQL_CHANNEL['name']] = name |  | ||||||
|         if download_directory is not None: |  | ||||||
|             verify_is_abspath(download_directory) |  | ||||||
|         data[SQL_CHANNEL['directory']] = download_directory |  | ||||||
| 
 |  | ||||||
|         self.cur.execute('INSERT INTO channels VALUES(?, ?, ?)', data) |  | ||||||
|         if get_videos: |  | ||||||
|             self.refresh_channel(channel_id, commit=False) |  | ||||||
|         if commit: |  | ||||||
|             self.sql.commit() |  | ||||||
| 
 |  | ||||||
|     def channel_has_pending(self, channel_id): |  | ||||||
|         query = 'SELECT * FROM videos WHERE author_id == ? AND download == "pending"' |  | ||||||
|         self.cur.execute(query, [channel_id]) |  | ||||||
|         return self.cur.fetchone() is not None |  | ||||||
| 
 |  | ||||||
|     def channel_directory(self, channel_id): |  | ||||||
|         self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) |  | ||||||
|         fetch = self.cur.fetchone() |  | ||||||
|         if fetch is None: |  | ||||||
|             return None |  | ||||||
|         return fetch[SQL_CHANNEL['directory']] |  | ||||||
| 
 |  | ||||||
|     def download_video(self, video, force=False): |  | ||||||
|         ''' |  | ||||||
|         Execute the `YOUTUBE_DL_COMMAND`, within the channel's associated |  | ||||||
|         directory if applicable. |  | ||||||
|         ''' |  | ||||||
|         # This logic is a little hazier than I would like, but it's all in the |  | ||||||
|         # interest of minimizing unnecessary API calls. |  | ||||||
|         if isinstance(video, ytapi.Video): |  | ||||||
|             video_id = video.id |  | ||||||
|         else: |  | ||||||
|             video_id = video |  | ||||||
|         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) |  | ||||||
|         video_row = self.cur.fetchone() |  | ||||||
|         if video_row is None: |  | ||||||
|             # Since the video was not in the db, we may not know about the channel either. |  | ||||||
|             if not isinstance(video, ytapi.Video): |  | ||||||
|                 print('get video') |  | ||||||
|                 video = self.youtube.get_video(video) |  | ||||||
|             channel_id = video.author_id |  | ||||||
|             self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) |  | ||||||
|             if self.cur.fetchone() is None: |  | ||||||
|                 print('add channel') |  | ||||||
|                 self.add_channel(channel_id, get_videos=False, commit=False) |  | ||||||
|             video_row = self.insert_video(video, commit=False)['row'] |  | ||||||
|         else: |  | ||||||
|             channel_id = video_row[SQL_VIDEO['author_id']] |  | ||||||
| 
 |  | ||||||
|         if video_row[SQL_VIDEO['download']] != 'pending' and not force: |  | ||||||
|             print('That video does not need to be downloaded.') |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         current_directory = os.getcwd() |  | ||||||
|         download_directory = self.channel_directory(channel_id) |  | ||||||
|         download_directory = download_directory or current_directory |  | ||||||
| 
 |  | ||||||
|         os.makedirs(download_directory, exist_ok=True) |  | ||||||
|         os.chdir(download_directory) |  | ||||||
| 
 |  | ||||||
|         self.youtube_dl_function(video_id) |  | ||||||
| 
 |  | ||||||
|         os.chdir(current_directory) |  | ||||||
| 
 |  | ||||||
|         self.cur.execute('UPDATE videos SET download = "downloaded" WHERE id == ?', [video_id]) |  | ||||||
|         self.sql.commit() |  | ||||||
| 
 |  | ||||||
|     def get_channel(self, channel_id): |  | ||||||
|         self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) |  | ||||||
|         fetch = self.cur.fetchone() |  | ||||||
|         if not fetch: |  | ||||||
|             return None |  | ||||||
|         fetch = {key: fetch[SQL_CHANNEL[key]] for key in SQL_CHANNEL} |  | ||||||
|         return fetch |  | ||||||
| 
 |  | ||||||
|     def get_channels(self): |  | ||||||
|         self.cur.execute('SELECT * FROM channels') |  | ||||||
|         channels = self.cur.fetchall() |  | ||||||
|         channels = [{key: channel[SQL_CHANNEL[key]] for key in SQL_CHANNEL} for channel in channels] |  | ||||||
|         channels.sort(key=lambda x: x['name'].lower()) |  | ||||||
|         return channels |  | ||||||
| 
 |  | ||||||
|     def get_video(self, video_id): |  | ||||||
|         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) |  | ||||||
|         video = self.cur.fetchone() |  | ||||||
|         video = {key: video[SQL_VIDEO[key]] for key in SQL_VIDEO} |  | ||||||
|         return video |  | ||||||
| 
 |  | ||||||
|     def get_videos(self, channel_id=None, download_filter=None): |  | ||||||
|         wheres = [] |  | ||||||
|         bindings = [] |  | ||||||
|         if channel_id is not None: |  | ||||||
|             wheres.append('author_id') |  | ||||||
|             bindings.append(channel_id) |  | ||||||
| 
 |  | ||||||
|         if download_filter is not None: |  | ||||||
|             wheres.append('download') |  | ||||||
|             bindings.append(download_filter) |  | ||||||
| 
 |  | ||||||
|         if wheres: |  | ||||||
|             wheres = [x + ' == ?' for x in wheres] |  | ||||||
|             wheres = ' WHERE ' + ' AND '.join(wheres) |  | ||||||
|         else: |  | ||||||
|             wheres = '' |  | ||||||
| 
 |  | ||||||
|         query = 'SELECT * FROM videos' + wheres |  | ||||||
|         self.cur.execute(query, bindings) |  | ||||||
|         videos = self.cur.fetchall() |  | ||||||
|         if not videos: |  | ||||||
|             return [] |  | ||||||
| 
 |  | ||||||
|         videos = [{key: video[SQL_VIDEO[key]] for key in SQL_VIDEO} for video in videos] |  | ||||||
|         videos.sort(key=lambda x: x['published'], reverse=True) |  | ||||||
|         return videos |  | ||||||
| 
 |  | ||||||
|     def insert_video(self, video, *, add_channel=True, commit=True): |  | ||||||
|         if not isinstance(video, ytapi.Video): |  | ||||||
|             video = self.youtube.get_video(video) |  | ||||||
| 
 |  | ||||||
|         if add_channel: |  | ||||||
|             self.add_channel(video.author_id, get_videos=False, commit=False) |  | ||||||
|         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video.id]) |  | ||||||
|         fetch = self.cur.fetchone() |  | ||||||
|         if fetch is not None: |  | ||||||
|             return {'new': False, 'row': fetch} |  | ||||||
| 
 |  | ||||||
|         data = [None] * len(SQL_VIDEO) |  | ||||||
|         data[SQL_VIDEO['id']] = video.id |  | ||||||
|         data[SQL_VIDEO['published']] = video.published |  | ||||||
|         data[SQL_VIDEO['author_id']] = video.author_id |  | ||||||
|         data[SQL_VIDEO['title']] = video.title |  | ||||||
|         data[SQL_VIDEO['description']] = video.description |  | ||||||
|         data[SQL_VIDEO['thumbnail']] = video.thumbnail['url'] |  | ||||||
|         data[SQL_VIDEO['download']] = 'pending' |  | ||||||
| 
 |  | ||||||
|         self.cur.execute('INSERT INTO videos VALUES(?, ?, ?, ?, ?, ?, ?)', data) |  | ||||||
|         if commit: |  | ||||||
|             self.sql.commit() |  | ||||||
|         return {'new': True, 'row': data} |  | ||||||
| 
 |  | ||||||
|     def mark_video_state(self, video_id, state, commit=True): |  | ||||||
|         ''' |  | ||||||
|         Mark the video as ignored, pending, or downloaded. |  | ||||||
|         ''' |  | ||||||
|         if state not in ['ignored', 'pending', 'downloaded']: |  | ||||||
|             raise InvalidVideoState(state) |  | ||||||
|         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) |  | ||||||
|         if self.cur.fetchone() is None: |  | ||||||
|             raise NoSuchVideo(video_id) |  | ||||||
|         self.cur.execute('UPDATE videos SET download = ? WHERE id == ?', [state, video_id]) |  | ||||||
|         if commit: |  | ||||||
|             self.sql.commit() |  | ||||||
| 
 |  | ||||||
|     def refresh_all_channels(self, force=False, commit=True): |  | ||||||
|         for channel in self.get_channels(): |  | ||||||
|             self.refresh_channel(channel['id'], force=force, commit=commit) |  | ||||||
|         if commit: |  | ||||||
|             self.sql.commit() |  | ||||||
| 
 |  | ||||||
|     def refresh_channel(self, channel_id, force=False, commit=True): |  | ||||||
|         video_generator = self.youtube.get_user_videos(uid=channel_id) |  | ||||||
|         log.debug('Refreshing channel: %s', channel_id) |  | ||||||
|         for video in video_generator: |  | ||||||
|             status = self.insert_video(video, commit=False) |  | ||||||
|             if not force and not status['new']: |  | ||||||
|                 break |  | ||||||
|         if commit: |  | ||||||
|             self.sql.commit() |  | ||||||
|  |  | ||||||
							
								
								
									
										300
									
								
								ycdl/ycdl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								ycdl/ycdl.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,300 @@ | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import sqlite3 | ||||||
|  | 
 | ||||||
|  | from . import helpers | ||||||
|  | from . import ytapi | ||||||
|  | 
 | ||||||
|  | def YOUTUBE_DL_COMMAND(video_id): | ||||||
|  |     path = 'D:\\Incoming\\ytqueue\\{id}.ytqueue'.format(id=video_id) | ||||||
|  |     open(path, 'w') | ||||||
|  | 
 | ||||||
|  | logging.basicConfig(level=logging.DEBUG) | ||||||
|  | log = logging.getLogger(__name__) | ||||||
|  | logging.getLogger('googleapiclient.discovery').setLevel(logging.WARNING) | ||||||
|  | logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARNING) | ||||||
|  | logging.getLogger('requests.packages.urllib3.util.retry').setLevel(logging.WARNING) | ||||||
|  | 
 | ||||||
|  | SQL_CHANNEL_COLUMNS = [ | ||||||
|  |     'id', | ||||||
|  |     'name', | ||||||
|  |     'directory', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | SQL_VIDEO_COLUMNS = [ | ||||||
|  |     'id', | ||||||
|  |     'published', | ||||||
|  |     'author_id', | ||||||
|  |     'title', | ||||||
|  |     'description', | ||||||
|  |     'duration', | ||||||
|  |     'thumbnail', | ||||||
|  |     'download', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | SQL_CHANNEL = {key:index for (index, key) in enumerate(SQL_CHANNEL_COLUMNS)} | ||||||
|  | SQL_VIDEO = {key:index for (index, key) in enumerate(SQL_VIDEO_COLUMNS)} | ||||||
|  | 
 | ||||||
|  | DATABASE_VERSION = 2 | ||||||
|  | DB_INIT = ''' | ||||||
|  | PRAGMA count_changes = OFF; | ||||||
|  | PRAGMA cache_size = 10000; | ||||||
|  | PRAGMA user_version = {user_version}; | ||||||
|  | CREATE TABLE IF NOT EXISTS channels( | ||||||
|  |     id TEXT, | ||||||
|  |     name TEXT, | ||||||
|  |     directory TEXT COLLATE NOCASE | ||||||
|  | ); | ||||||
|  | CREATE TABLE IF NOT EXISTS videos( | ||||||
|  |     id TEXT, | ||||||
|  |     published INT, | ||||||
|  |     author_id TEXT, | ||||||
|  |     title TEXT, | ||||||
|  |     description TEXT, | ||||||
|  |     duration INT, | ||||||
|  |     thumbnail TEXT, | ||||||
|  |     download TEXT | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | CREATE INDEX IF NOT EXISTS index_channel_id on channels(id); | ||||||
|  | CREATE INDEX IF NOT EXISTS index_video_author on videos(author_id); | ||||||
|  | CREATE INDEX IF NOT EXISTS index_video_id on videos(id); | ||||||
|  | CREATE INDEX IF NOT EXISTS index_video_published on videos(published); | ||||||
|  | CREATE INDEX IF NOT EXISTS index_video_download on videos(download); | ||||||
|  | '''.format(user_version=DATABASE_VERSION) | ||||||
|  | 
 | ||||||
|  | DEFAULT_DBNAME = 'ycdl.db' | ||||||
|  | 
 | ||||||
|  | ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def verify_is_abspath(path): | ||||||
|  |     ''' | ||||||
|  |     TO DO: Determine whether this is actually correct. | ||||||
|  |     ''' | ||||||
|  |     if os.path.abspath(path) != path: | ||||||
|  |         raise ValueError('Not an abspath') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class InvalidVideoState(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | class NoSuchVideo(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class YCDL: | ||||||
|  |     def __init__(self, youtube, database_filename=None, youtube_dl_function=None): | ||||||
|  |         self.youtube = youtube | ||||||
|  |         if database_filename is None: | ||||||
|  |             database_filename = DEFAULT_DBNAME | ||||||
|  | 
 | ||||||
|  |         existing_database = os.path.exists(database_filename) | ||||||
|  |         self.sql = sqlite3.connect(database_filename) | ||||||
|  |         self.cur = self.sql.cursor() | ||||||
|  | 
 | ||||||
|  |         if existing_database: | ||||||
|  |             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 | ||||||
|  | 
 | ||||||
|  |         if youtube_dl_function: | ||||||
|  |             self.youtube_dl_function = youtube_dl_function | ||||||
|  |         else: | ||||||
|  |             self.youtube_dl_function = YOUTUBE_DL_COMMAND | ||||||
|  | 
 | ||||||
|  |         statements = DB_INIT.split(';') | ||||||
|  |         for statement in statements: | ||||||
|  |             self.cur.execute(statement) | ||||||
|  |         self.sql.commit() | ||||||
|  | 
 | ||||||
|  |     def add_channel( | ||||||
|  |             self, | ||||||
|  |             channel_id, | ||||||
|  |             commit=True, | ||||||
|  |             download_directory=None, | ||||||
|  |             get_videos=False, | ||||||
|  |             name=None, | ||||||
|  |         ): | ||||||
|  |         if self.get_channel(channel_id) is not None: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         if name is None: | ||||||
|  |             name = self.youtube.get_user_name(channel_id) | ||||||
|  | 
 | ||||||
|  |         data = [None] * len(SQL_CHANNEL) | ||||||
|  |         data[SQL_CHANNEL['id']] = channel_id | ||||||
|  |         data[SQL_CHANNEL['name']] = name | ||||||
|  |         if download_directory is not None: | ||||||
|  |             verify_is_abspath(download_directory) | ||||||
|  |         data[SQL_CHANNEL['directory']] = download_directory | ||||||
|  | 
 | ||||||
|  |         self.cur.execute('INSERT INTO channels VALUES(?, ?, ?)', data) | ||||||
|  |         if get_videos: | ||||||
|  |             self.refresh_channel(channel_id, commit=False) | ||||||
|  |         if commit: | ||||||
|  |             self.sql.commit() | ||||||
|  | 
 | ||||||
|  |     def channel_has_pending(self, channel_id): | ||||||
|  |         query = 'SELECT * FROM videos WHERE author_id == ? AND download == "pending"' | ||||||
|  |         self.cur.execute(query, [channel_id]) | ||||||
|  |         return self.cur.fetchone() is not None | ||||||
|  | 
 | ||||||
|  |     def channel_directory(self, channel_id): | ||||||
|  |         self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) | ||||||
|  |         fetch = self.cur.fetchone() | ||||||
|  |         if fetch is None: | ||||||
|  |             return None | ||||||
|  |         return fetch[SQL_CHANNEL['directory']] | ||||||
|  | 
 | ||||||
|  |     def download_video(self, video, force=False): | ||||||
|  |         ''' | ||||||
|  |         Execute the `YOUTUBE_DL_COMMAND`, within the channel's associated | ||||||
|  |         directory if applicable. | ||||||
|  |         ''' | ||||||
|  |         # This logic is a little hazier than I would like, but it's all in the | ||||||
|  |         # interest of minimizing unnecessary API calls. | ||||||
|  |         if isinstance(video, ytapi.Video): | ||||||
|  |             video_id = video.id | ||||||
|  |         else: | ||||||
|  |             video_id = video | ||||||
|  |         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) | ||||||
|  |         video_row = self.cur.fetchone() | ||||||
|  |         if video_row is None: | ||||||
|  |             # Since the video was not in the db, we may not know about the channel either. | ||||||
|  |             if not isinstance(video, ytapi.Video): | ||||||
|  |                 print('get video') | ||||||
|  |                 video = self.youtube.get_video(video) | ||||||
|  |             channel_id = video.author_id | ||||||
|  |             self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) | ||||||
|  |             if self.cur.fetchone() is None: | ||||||
|  |                 print('add channel') | ||||||
|  |                 self.add_channel(channel_id, get_videos=False, commit=False) | ||||||
|  |             video_row = self.insert_video(video, commit=False)['row'] | ||||||
|  |         else: | ||||||
|  |             channel_id = video_row[SQL_VIDEO['author_id']] | ||||||
|  | 
 | ||||||
|  |         if video_row[SQL_VIDEO['download']] != 'pending' and not force: | ||||||
|  |             print('That video does not need to be downloaded.') | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         current_directory = os.getcwd() | ||||||
|  |         download_directory = self.channel_directory(channel_id) | ||||||
|  |         download_directory = download_directory or current_directory | ||||||
|  | 
 | ||||||
|  |         os.makedirs(download_directory, exist_ok=True) | ||||||
|  |         os.chdir(download_directory) | ||||||
|  | 
 | ||||||
|  |         self.youtube_dl_function(video_id) | ||||||
|  | 
 | ||||||
|  |         os.chdir(current_directory) | ||||||
|  | 
 | ||||||
|  |         self.cur.execute('UPDATE videos SET download = "downloaded" WHERE id == ?', [video_id]) | ||||||
|  |         self.sql.commit() | ||||||
|  | 
 | ||||||
|  |     def get_channel(self, channel_id): | ||||||
|  |         self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id]) | ||||||
|  |         fetch = self.cur.fetchone() | ||||||
|  |         if not fetch: | ||||||
|  |             return None | ||||||
|  |         fetch = {key: fetch[SQL_CHANNEL[key]] for key in SQL_CHANNEL} | ||||||
|  |         return fetch | ||||||
|  | 
 | ||||||
|  |     def get_channels(self): | ||||||
|  |         self.cur.execute('SELECT * FROM channels') | ||||||
|  |         channels = self.cur.fetchall() | ||||||
|  |         channels = [{key: channel[SQL_CHANNEL[key]] for key in SQL_CHANNEL} for channel in channels] | ||||||
|  |         channels.sort(key=lambda x: x['name'].lower()) | ||||||
|  |         return channels | ||||||
|  | 
 | ||||||
|  |     def get_video(self, video_id): | ||||||
|  |         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) | ||||||
|  |         video = self.cur.fetchone() | ||||||
|  |         video = {key: video[SQL_VIDEO[key]] for key in SQL_VIDEO} | ||||||
|  |         return video | ||||||
|  | 
 | ||||||
|  |     def get_videos(self, channel_id=None, download_filter=None): | ||||||
|  |         wheres = [] | ||||||
|  |         bindings = [] | ||||||
|  |         if channel_id is not None: | ||||||
|  |             wheres.append('author_id') | ||||||
|  |             bindings.append(channel_id) | ||||||
|  | 
 | ||||||
|  |         if download_filter is not None: | ||||||
|  |             wheres.append('download') | ||||||
|  |             bindings.append(download_filter) | ||||||
|  | 
 | ||||||
|  |         if wheres: | ||||||
|  |             wheres = [x + ' == ?' for x in wheres] | ||||||
|  |             wheres = ' WHERE ' + ' AND '.join(wheres) | ||||||
|  |         else: | ||||||
|  |             wheres = '' | ||||||
|  | 
 | ||||||
|  |         query = 'SELECT * FROM videos' + wheres | ||||||
|  |         self.cur.execute(query, bindings) | ||||||
|  |         videos = self.cur.fetchall() | ||||||
|  |         if not videos: | ||||||
|  |             return [] | ||||||
|  | 
 | ||||||
|  |         videos = [{key: video[SQL_VIDEO[key]] for key in SQL_VIDEO} for video in videos] | ||||||
|  |         videos.sort(key=lambda x: x['published'], reverse=True) | ||||||
|  |         return videos | ||||||
|  | 
 | ||||||
|  |     def insert_video(self, video, *, add_channel=True, commit=True): | ||||||
|  |         if not isinstance(video, ytapi.Video): | ||||||
|  |             video = self.youtube.get_video(video) | ||||||
|  | 
 | ||||||
|  |         if add_channel: | ||||||
|  |             self.add_channel(video.author_id, get_videos=False, commit=False) | ||||||
|  |         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video.id]) | ||||||
|  |         fetch = self.cur.fetchone() | ||||||
|  |         if fetch is not None: | ||||||
|  |             return {'new': False, 'row': fetch} | ||||||
|  | 
 | ||||||
|  |         data = [None] * len(SQL_VIDEO) | ||||||
|  |         data[SQL_VIDEO['id']] = video.id | ||||||
|  |         data[SQL_VIDEO['published']] = video.published | ||||||
|  |         data[SQL_VIDEO['author_id']] = video.author_id | ||||||
|  |         data[SQL_VIDEO['title']] = video.title | ||||||
|  |         data[SQL_VIDEO['description']] = video.description | ||||||
|  |         data[SQL_VIDEO['duration']] = video.duration | ||||||
|  |         data[SQL_VIDEO['thumbnail']] = video.thumbnail['url'] | ||||||
|  |         data[SQL_VIDEO['download']] = 'pending' | ||||||
|  | 
 | ||||||
|  |         self.cur.execute('INSERT INTO videos VALUES(?, ?, ?, ?, ?, ?, ?, ?)', data) | ||||||
|  |         if commit: | ||||||
|  |             self.sql.commit() | ||||||
|  |         return {'new': True, 'row': data} | ||||||
|  | 
 | ||||||
|  |     def mark_video_state(self, video_id, state, commit=True): | ||||||
|  |         ''' | ||||||
|  |         Mark the video as ignored, pending, or downloaded. | ||||||
|  |         ''' | ||||||
|  |         if state not in ['ignored', 'pending', 'downloaded']: | ||||||
|  |             raise InvalidVideoState(state) | ||||||
|  |         self.cur.execute('SELECT * FROM videos WHERE id == ?', [video_id]) | ||||||
|  |         if self.cur.fetchone() is None: | ||||||
|  |             raise NoSuchVideo(video_id) | ||||||
|  |         self.cur.execute('UPDATE videos SET download = ? WHERE id == ?', [state, video_id]) | ||||||
|  |         if commit: | ||||||
|  |             self.sql.commit() | ||||||
|  | 
 | ||||||
|  |     def refresh_all_channels(self, force=False, commit=True): | ||||||
|  |         for channel in self.get_channels(): | ||||||
|  |             self.refresh_channel(channel['id'], force=force, commit=commit) | ||||||
|  |         if commit: | ||||||
|  |             self.sql.commit() | ||||||
|  | 
 | ||||||
|  |     def refresh_channel(self, channel_id, force=False, commit=True): | ||||||
|  |         video_generator = self.youtube.get_user_videos(uid=channel_id) | ||||||
|  |         log.debug('Refreshing channel: %s', channel_id) | ||||||
|  |         for video in video_generator: | ||||||
|  |             status = self.insert_video(video, commit=False) | ||||||
|  |             if not force and not status['new']: | ||||||
|  |                 break | ||||||
|  |         if commit: | ||||||
|  |             self.sql.commit() | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import apiclient.discovery | import apiclient.discovery | ||||||
| import datetime | import datetime | ||||||
|  | import isodate | ||||||
| 
 | 
 | ||||||
| from . import helpers | from . import helpers | ||||||
| 
 | 
 | ||||||
|  | @ -7,21 +8,24 @@ class VideoNotFound(Exception): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| class Video: | class Video: | ||||||
|     def __init__(self, snippet): |     def __init__(self, data): | ||||||
|         self.id = snippet['id'] |         self.id = data['id'] | ||||||
|  | 
 | ||||||
|  |         snippet = data['snippet'] | ||||||
|  |         content_details = data['contentDetails'] | ||||||
| 
 | 
 | ||||||
|         snippet = snippet['snippet'] |  | ||||||
|         self.title = snippet['title'] or '[untitled]' |         self.title = snippet['title'] or '[untitled]' | ||||||
|         self.description = snippet['description'] |         self.description = snippet['description'] | ||||||
|         self.author_id = snippet['channelId'] |         self.author_id = snippet['channelId'] | ||||||
|         self.author_name = snippet['channelTitle'] |         self.author_name = snippet['channelTitle'] | ||||||
|         # Something like '2016-10-01T21:00:01' |         # Something like '2016-10-01T21:00:01' | ||||||
|         self.published_string = snippet['publishedAt'] |         self.published_string = snippet['publishedAt'] | ||||||
|         published = snippet['publishedAt'] |         published = snippet['publishedAt'].split('.')[0] | ||||||
|         published = published.split('.')[0] |  | ||||||
|         published = datetime.datetime.strptime(published, '%Y-%m-%dT%H:%M:%S') |         published = datetime.datetime.strptime(published, '%Y-%m-%dT%H:%M:%S') | ||||||
|         self.published = published.timestamp() |         self.published = published.timestamp() | ||||||
| 
 | 
 | ||||||
|  |         self.duration = isodate.parse_duration(content_details['duration']).seconds | ||||||
|  | 
 | ||||||
|         thumbnails = snippet['thumbnails'] |         thumbnails = snippet['thumbnails'] | ||||||
|         best_thumbnail = max(thumbnails, key=lambda x: thumbnails[x]['width'] * thumbnails[x]['height']) |         best_thumbnail = max(thumbnails, key=lambda x: thumbnails[x]['width'] * thumbnails[x]['height']) | ||||||
|         self.thumbnail = thumbnails[best_thumbnail] |         self.thumbnail = thumbnails[best_thumbnail] | ||||||
|  | @ -54,38 +58,39 @@ class Youtube: | ||||||
|             user = self.youtube.channels().list(part='contentDetails', id=uid).execute() |             user = self.youtube.channels().list(part='contentDetails', id=uid).execute() | ||||||
|         upload_playlist = user['items'][0]['contentDetails']['relatedPlaylists']['uploads'] |         upload_playlist = user['items'][0]['contentDetails']['relatedPlaylists']['uploads'] | ||||||
|         page_token = None |         page_token = None | ||||||
|  |         total = 0 | ||||||
|         while True: |         while True: | ||||||
|             items = self.youtube.playlistItems().list( |             response = self.youtube.playlistItems().list( | ||||||
|                 maxResults=50, |                 maxResults=50, | ||||||
|                 pageToken=page_token, |                 pageToken=page_token, | ||||||
|                 part='contentDetails', |                 part='contentDetails', | ||||||
|                 playlistId=upload_playlist, |                 playlistId=upload_playlist, | ||||||
|             ).execute() |             ).execute() | ||||||
|             page_token = items.get('nextPageToken', None) |             page_token = response.get('nextPageToken', None) | ||||||
|             new = [item['contentDetails']['videoId'] for item in items['items']] |             video_ids = [item['contentDetails']['videoId'] for item in response['items']] | ||||||
|             count = len(new) |             videos = self.get_video(video_ids) | ||||||
|             new = self.get_video(new) |             videos.sort(key=lambda x: x.published, reverse=True) | ||||||
|             new.sort(key=lambda x: x.published, reverse=True) |             yield from videos | ||||||
|             yield from new | 
 | ||||||
|             #print('Found %d more, %d total' % (count, len(videos))) |             count = len(videos) | ||||||
|  |             total += count | ||||||
|  |             print(f'Found {count} more, {total} total') | ||||||
|             if page_token is None or count < 50: |             if page_token is None or count < 50: | ||||||
|                 break |                 break | ||||||
| 
 | 
 | ||||||
|     def get_related_videos(self, video_id, part='id,snippet', count=50): |     def get_related_videos(self, video_id, count=50): | ||||||
|         if isinstance(video_id, Video): |         if isinstance(video_id, Video): | ||||||
|             video_id = video_id.id |             video_id = video_id.id | ||||||
| 
 | 
 | ||||||
|         results = self.youtube.search().list( |         results = self.youtube.search().list( | ||||||
|             part=part, |             part='id', | ||||||
|             relatedToVideoId=video_id, |             relatedToVideoId=video_id, | ||||||
|             type='video', |             type='video', | ||||||
|             maxResults=count, |             maxResults=count, | ||||||
|         ).execute() |         ).execute() | ||||||
|         videos = [] |         videos = [] | ||||||
|         for related in results['items']: |         related = [rel['id']['videoId'] for rel in results['items']] | ||||||
|             related['id'] = related['id']['videoId'] |         videos = self.get_video(related) | ||||||
|             video = Video(related) |  | ||||||
|             videos.append(video) |  | ||||||
|         return videos |         return videos | ||||||
| 
 | 
 | ||||||
|     def get_video(self, video_ids): |     def get_video(self, video_ids): | ||||||
|  | @ -99,7 +104,7 @@ class Youtube: | ||||||
|         chunks = helpers.chunk_sequence(video_ids, 50) |         chunks = helpers.chunk_sequence(video_ids, 50) | ||||||
|         for chunk in chunks: |         for chunk in chunks: | ||||||
|             chunk = ','.join(chunk) |             chunk = ','.join(chunk) | ||||||
|             data = self.youtube.videos().list(part='snippet', id=chunk).execute() |             data = self.youtube.videos().list(part='id,contentDetails,snippet', id=chunk).execute() | ||||||
|             items = data['items'] |             items = data['items'] | ||||||
|             results.extend(items) |             results.extend(items) | ||||||
|         results = [Video(snippet) for snippet in results] |         results = [Video(snippet) for snippet in results] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue