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